diff --git a/account.go b/account.go index 234e1ee..c7f8105 100644 --- a/account.go +++ b/account.go @@ -1,1548 +1,1549 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "encoding/json" "fmt" + "github.com/writefreely/writefreely/mailer" + "github.com/writefreely/writefreely/spam" "html/template" "net/http" "regexp" "strconv" "strings" "sync" "time" "github.com/gorilla/csrf" "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/guregu/null/zero" "github.com/writeas/impart" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/data" "github.com/writeas/web-core/log" "github.com/writefreely/writefreely/author" "github.com/writefreely/writefreely/config" - "github.com/writefreely/writefreely/mailer" "github.com/writefreely/writefreely/page" - "github.com/writefreely/writefreely/spam" ) type ( userSettings struct { Username string `schema:"username" json:"username"` Email string `schema:"email" json:"email"` NewPass string `schema:"new-pass" json:"new_pass"` OldPass string `schema:"current-pass" json:"current_pass"` IsLogOut bool `schema:"logout" json:"logout"` } UserPage struct { page.StaticPage PageTitle string Separator template.HTML IsAdmin bool CanInvite bool CollAlias string } ) func NewUserPage(app *App, r *http.Request, u *User, title string, flashes []string) *UserPage { up := &UserPage{ StaticPage: pageForReq(app, r), PageTitle: title, } up.Username = u.Username up.Flashes = flashes up.Path = r.URL.Path up.IsAdmin = u.IsAdmin() up.CanInvite = canUserInvite(app.cfg, up.IsAdmin) return up } func canUserInvite(cfg *config.Config, isAdmin bool) bool { return cfg.App.UserInvites != "" && (isAdmin || cfg.App.UserInvites != "admin") } func (up *UserPage) SetMessaging(u *User) { // up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID) } const ( loginAttemptExpiration = 3 * time.Second ) var actuallyUsernameReg = regexp.MustCompile("username is actually ([a-z0-9\\-]+)\\. Please try that, instead") func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error { _, err := signup(app, w, r) return err } func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) { if app.cfg.App.DisablePasswordAuth { err := ErrDisabledPasswordAuth return nil, err } reqJSON := IsJSON(r) // Get params var ur userRegistration if reqJSON { decoder := json.NewDecoder(r.Body) err := decoder.Decode(&ur) if err != nil { log.Error("Couldn't parse signup JSON request: %v\n", err) return nil, ErrBadJSON } } else { // Check if user is already logged in u := getUserSession(app, r) if u != nil { return &AuthUser{User: u}, nil } err := r.ParseForm() if err != nil { log.Error("Couldn't parse signup form request: %v\n", err) return nil, ErrBadFormData } err = app.formDecoder.Decode(&ur, r.PostForm) if err != nil { log.Error("Couldn't decode signup form request: %v\n", err) return nil, ErrBadFormData } } return signupWithRegistration(app, ur, w, r) } func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWriter, r *http.Request) (*AuthUser, error) { reqJSON := IsJSON(r) // Validate required params (alias) if signup.Alias == "" { return nil, impart.HTTPError{http.StatusBadRequest, "A username is required."} } if signup.Pass == "" { return nil, impart.HTTPError{http.StatusBadRequest, "A password is required."} } var desiredUsername string if signup.Normalize { // With this option we simply conform the username to what we expect // without complaining. Since they might've done something funny, like // enter: write.as/Way Out There, we'll use their raw input for the new // collection name and sanitize for the slug / username. desiredUsername = signup.Alias signup.Alias = getSlug(signup.Alias, "") } if !author.IsValidUsername(app.cfg, signup.Alias) { // Ensure the username is syntactically correct. return nil, impart.HTTPError{http.StatusPreconditionFailed, "Username is reserved or isn't valid. It must be at least 3 characters long, and can only include letters, numbers, and hyphens."} } // Handle empty optional params hashedPass, err := auth.HashPass([]byte(signup.Pass)) if err != nil { return nil, impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."} } // Create struct to insert u := &User{ Username: signup.Alias, HashedPass: hashedPass, HasPass: true, Email: prepareUserEmail(signup.Email, app.keys.EmailKey), Created: time.Now().Truncate(time.Second).UTC(), } // Create actual user if err := app.db.CreateUser(app.cfg, u, desiredUsername, signup.Description); err != nil { return nil, err } // Log invite if needed if signup.InviteCode != "" { err = app.db.CreateInvitedUser(signup.InviteCode, u.ID) if err != nil { return nil, err } } // Add back unencrypted data for response if signup.Email != "" { u.Email.String = signup.Email } resUser := &AuthUser{ User: u, } title := signup.Alias if signup.Normalize { title = desiredUsername } resUser.Collections = &[]Collection{ { Alias: signup.Alias, Title: title, Description: signup.Description, }, } var coll *Collection if signup.Monetization != "" { if coll == nil { coll, err = app.db.GetCollection(signup.Alias) if err != nil { log.Error("Unable to get new collection '%s' for monetization on signup: %v", signup.Alias, err) return nil, err } } err = app.db.SetCollectionAttribute(coll.ID, "monetization_pointer", signup.Monetization) if err != nil { log.Error("Unable to add monetization on signup: %v", err) return nil, err } coll.Monetization = signup.Monetization } var token string if reqJSON && !signup.Web { token, err = app.db.GetAccessToken(u.ID) if err != nil { return nil, impart.HTTPError{http.StatusInternalServerError, "Could not create access token. Try re-authenticating."} } resUser.AccessToken = token } else { session, err := app.sessionStore.Get(r, cookieName) if err != nil { // The cookie should still save, even if there's an error. // Source: https://github.com/gorilla/sessions/issues/16#issuecomment-143642144 log.Error("Session: %v; ignoring", err) } session.Values[cookieUserVal] = resUser.User.Cookie() err = session.Save(r, w) if err != nil { log.Error("Couldn't save session: %v", err) return nil, err } } if reqJSON { return resUser, impart.WriteSuccess(w, resUser, http.StatusCreated) } return resUser, nil } func viewLogout(app *App, w http.ResponseWriter, r *http.Request) error { session, err := app.sessionStore.Get(r, cookieName) if err != nil { return ErrInternalCookieSession } // Ensure user has an email or password set before they go, so they don't // lose access to their account. val := session.Values[cookieUserVal] var u = &User{} var ok bool if u, ok = val.(*User); !ok { log.Error("Error casting user object on logout. Vals: %+v Resetting cookie.", session.Values) err = session.Save(r, w) if err != nil { log.Error("Couldn't save session on logout: %v", err) return impart.HTTPError{http.StatusInternalServerError, "Unable to save cookie session."} } return impart.HTTPError{http.StatusFound, "/"} } u, err = app.db.GetUserByID(u.ID) if err != nil && err != ErrUserNotFound { return impart.HTTPError{http.StatusInternalServerError, "Unable to fetch user information."} } session.Options.MaxAge = -1 err = session.Save(r, w) if err != nil { log.Error("Couldn't save session on logout: %v", err) return impart.HTTPError{http.StatusInternalServerError, "Unable to save cookie session."} } return impart.HTTPError{http.StatusFound, "/"} } func handleAPILogout(app *App, w http.ResponseWriter, r *http.Request) error { accessToken := r.Header.Get("Authorization") if accessToken == "" { return ErrNoAccessToken } t := auth.GetToken(accessToken) if len(t) == 0 { return ErrNoAccessToken } err := app.db.DeleteToken(t) if err != nil { return err } return impart.HTTPError{Status: http.StatusNoContent} } func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { var earlyError string oneTimeToken := r.FormValue("with") if oneTimeToken != "" { log.Info("Calling login with one-time token.") err := login(app, w, r) if err != nil { log.Info("Received error: %v", err) earlyError = fmt.Sprintf("%s", err) } } session, err := app.sessionStore.Get(r, cookieName) if err != nil { // Ignore this log.Error("Unable to get session; ignoring: %v", err) } p := &struct { page.StaticPage *OAuthButtons To string Message template.HTML Flashes []template.HTML EmailEnabled bool LoginUsername string }{ StaticPage: pageForReq(app, r), OAuthButtons: NewOAuthButtons(app.Config()), To: r.FormValue("to"), Message: template.HTML(""), Flashes: []template.HTML{}, EmailEnabled: app.cfg.Email.Enabled(), LoginUsername: getTempInfo(app, "login-user", r, w), } if earlyError != "" { p.Flashes = append(p.Flashes, template.HTML(earlyError)) } // Display any error messages flashes, _ := getSessionFlashes(app, w, r, session) for _, flash := range flashes { p.Flashes = append(p.Flashes, template.HTML(flash)) } err = pages["login.tmpl"].ExecuteTemplate(w, "base", p) if err != nil { log.Error("Unable to render login: %v", err) return err } return nil } func webLogin(app *App, w http.ResponseWriter, r *http.Request) error { err := login(app, w, r) if err != nil { username := r.FormValue("alias") // Login request was unsuccessful; save the error in the session and redirect them if err, ok := err.(impart.HTTPError); ok { session, _ := app.sessionStore.Get(r, cookieName) if session != nil { session.AddFlash(err.Message) session.Save(r, w) } if m := actuallyUsernameReg.FindStringSubmatch(err.Message); len(m) > 0 { // Retain fixed username recommendation for the login form username = m[1] } } // Pass along certain information saveTempInfo(app, "login-user", username, r, w) // Retain post-login URL if one was given redirectTo := "/login" postLoginRedirect := r.FormValue("to") if postLoginRedirect != "" { redirectTo += "?to=" + postLoginRedirect } log.Error("Unable to login: %v", err) return impart.HTTPError{http.StatusTemporaryRedirect, redirectTo} } return nil } var loginAttemptUsers = sync.Map{} func login(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r) oneTimeToken := r.FormValue("with") verbose := r.FormValue("all") == "true" || r.FormValue("verbose") == "1" || r.FormValue("verbose") == "true" || (reqJSON && oneTimeToken != "") redirectTo := r.FormValue("to") if redirectTo == "" { if app.cfg.App.SingleUser { redirectTo = "/me/new" } else { redirectTo = "/" } } var u *User var err error var signin userCredentials if app.cfg.App.DisablePasswordAuth { err := ErrDisabledPasswordAuth return err } // Log in with one-time token if one is given if oneTimeToken != "" { log.Info("Login: Logging user in via token.") userID := app.db.GetUserID(oneTimeToken) if userID == -1 { log.Error("Login: Got user -1 from token") err := ErrBadAccessToken err.Message = "Expired or invalid login code." return err } log.Info("Login: Found user %d.", userID) u, err = app.db.GetUserByID(userID) if err != nil { log.Error("Unable to fetch user on one-time token login: %v", err) return impart.HTTPError{http.StatusInternalServerError, "There was an error retrieving the user you want."} } log.Info("Login: Got user via token") } else { // Get params if reqJSON { decoder := json.NewDecoder(r.Body) err := decoder.Decode(&signin) if err != nil { log.Error("Couldn't parse signin JSON request: %v\n", err) return ErrBadJSON } } else { err := r.ParseForm() if err != nil { log.Error("Couldn't parse signin form request: %v\n", err) return ErrBadFormData } err = app.formDecoder.Decode(&signin, r.PostForm) if err != nil { log.Error("Couldn't decode signin form request: %v\n", err) return ErrBadFormData } } log.Info("Login: Attempting login for '%s'", signin.Alias) // Validate required params (all) if signin.Alias == "" { msg := "Parameter `alias` required." if signin.Web { msg = "A username is required." } return impart.HTTPError{http.StatusBadRequest, msg} } if !signin.EmailLogin && signin.Pass == "" { msg := "Parameter `pass` required." if signin.Web { msg = "A password is required." } return impart.HTTPError{http.StatusBadRequest, msg} } // Prevent excessive login attempts on the same account // Skip this check in dev environment if !app.cfg.Server.Dev { now := time.Now() attemptExp, att := loginAttemptUsers.LoadOrStore(signin.Alias, now.Add(loginAttemptExpiration)) if att { if attemptExpTime, ok := attemptExp.(time.Time); ok { if attemptExpTime.After(now) { // This user attempted previously, and the period hasn't expired yet return impart.HTTPError{http.StatusTooManyRequests, "You're doing that too much."} } else { // This user attempted previously, but the time expired; free up space loginAttemptUsers.Delete(signin.Alias) } } else { log.Error("Unable to cast expiration to time") } } } // Retrieve password u, err = app.db.GetUserForAuth(signin.Alias) if err != nil { log.Info("Unable to getUserForAuth on %s: %v", signin.Alias, err) if strings.IndexAny(signin.Alias, "@") > 0 { log.Info("Suggesting: %s", ErrUserNotFoundEmail.Message) return ErrUserNotFoundEmail } return err } // Authenticate if u.Email.String == "" { // User has no email set, so check if they haven't added a password, either, // so we can return a more helpful error message. if hasPass, _ := app.db.IsUserPassSet(u.ID); !hasPass { log.Info("Tried logging into %s, but no password or email.", signin.Alias) return impart.HTTPError{http.StatusPreconditionFailed, "This user never added a password or email address. Please contact us for help."} } } if len(u.HashedPass) == 0 { return impart.HTTPError{http.StatusUnauthorized, "This user never set a password. Perhaps try logging in via OAuth?"} } if !auth.Authenticated(u.HashedPass, []byte(signin.Pass)) { return impart.HTTPError{http.StatusUnauthorized, "Incorrect password."} } } if reqJSON && !signin.Web { var token string if r.Header.Get("User-Agent") == "" { // Get last created token when User-Agent is empty token = app.db.FetchLastAccessToken(u.ID) if token == "" { token, err = app.db.GetAccessToken(u.ID) } } else { token, err = app.db.GetAccessToken(u.ID) } if err != nil { log.Error("Login: Unable to create access token: %v", err) return impart.HTTPError{http.StatusInternalServerError, "Could not create access token. Try re-authenticating."} } resUser := getVerboseAuthUser(app, token, u, verbose) return impart.WriteSuccess(w, resUser, http.StatusOK) } session, err := app.sessionStore.Get(r, cookieName) if err != nil { // The cookie should still save, even if there's an error. log.Error("Login: Session: %v; ignoring", err) } // Remove unwanted data session.Values[cookieUserVal] = u.Cookie() err = session.Save(r, w) if err != nil { log.Error("Login: Couldn't save session: %v", err) // TODO: return error } // Send success if reqJSON { return impart.WriteSuccess(w, &AuthUser{User: u}, http.StatusOK) } + redirectTo = app.cfg.App.PrefixPath(redirectTo) log.Info("Login: Redirecting to %s", redirectTo) + redirectTo = app.cfg.App.PrefixPath(redirectTo) w.Header().Set("Location", redirectTo) w.WriteHeader(http.StatusFound) return nil } func getVerboseAuthUser(app *App, token string, u *User, verbose bool) *AuthUser { resUser := &AuthUser{ AccessToken: token, User: u, } // Fetch verbose user data if requested if verbose { posts, err := app.db.GetUserPosts(u) if err != nil { log.Error("Login: Unable to get user posts: %v", err) } colls, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { log.Error("Login: Unable to get user collections: %v", err) } passIsSet, err := app.db.IsUserPassSet(u.ID) if err != nil { // TODO: correct error message log.Error("Login: Unable to get user collections: %v", err) } resUser.Posts = posts resUser.Collections = colls resUser.User.HasPass = passIsSet } return resUser } func viewExportOptions(app *App, u *User, w http.ResponseWriter, r *http.Request) error { // Fetch extra user data p := NewUserPage(app, r, u, "Export", nil) showUserPage(w, "export", p) return nil } func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) { var filename string var u = &User{} reqJSON := IsJSON(r) if reqJSON { // Use given Authorization header accessToken := r.Header.Get("Authorization") if accessToken == "" { return nil, filename, ErrNoAccessToken } userID := app.db.GetUserID(accessToken) if userID == -1 { return nil, filename, ErrBadAccessToken } var err error u, err = app.db.GetUserByID(userID) if err != nil { return nil, filename, impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve requested user."} } } else { // Use user cookie session, err := app.sessionStore.Get(r, cookieName) if err != nil { // The cookie should still save, even if there's an error. log.Error("Session: %v; ignoring", err) } val := session.Values[cookieUserVal] var ok bool if u, ok = val.(*User); !ok { return nil, filename, ErrNotLoggedIn } } filename = u.Username + "-posts-" + time.Now().Truncate(time.Second).UTC().Format("200601021504") // Fetch data we're exporting var err error var data []byte posts, err := app.db.GetUserPosts(u) if err != nil { return data, filename, err } // Export as CSV if strings.HasSuffix(r.URL.Path, ".csv") { data = exportPostsCSV(app.cfg.App.Host, u, posts) return data, filename, err } if strings.HasSuffix(r.URL.Path, ".zip") { data = exportPostsZip(u, posts) return data, filename, err } if r.FormValue("pretty") == "1" { data, err = json.MarshalIndent(posts, "", "\t") } else { data, err = json.Marshal(posts) } return data, filename, err } func viewExportFull(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) { var err error filename := "" u := getUserSession(app, r) if u == nil { return nil, filename, ErrNotLoggedIn } filename = u.Username + "-" + time.Now().Truncate(time.Second).UTC().Format("200601021504") exportUser := compileFullExport(app, u) var data []byte if r.FormValue("pretty") == "1" { data, err = json.MarshalIndent(exportUser, "", "\t") } else { data, err = json.Marshal(exportUser) } return data, filename, err } func viewMeAPI(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r) uObj := struct { ID int64 `json:"id,omitempty"` Username string `json:"username,omitempty"` }{} var err error if reqJSON { _, uObj.Username, err = app.db.GetUserDataFromToken(r.Header.Get("Authorization")) if err != nil { return err } } else { u := getUserSession(app, r) if u == nil { return impart.WriteSuccess(w, uObj, http.StatusOK) } uObj.Username = u.Username } return impart.WriteSuccess(w, uObj, http.StatusOK) } func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r) if !reqJSON { return ErrBadRequestedType } isAnonPosts := r.FormValue("anonymous") == "1" if isAnonPosts { pageStr := r.FormValue("page") pg, err := strconv.Atoi(pageStr) if err != nil { log.Error("Error parsing page parameter '%s': %s", pageStr, err) pg = 1 } p, err := app.db.GetAnonymousPosts(u, pg) if err != nil { return err } return impart.WriteSuccess(w, p, http.StatusOK) } var err error p := GetPostsCache(u.ID) if p == nil { userPostsCache.Lock() if userPostsCache.users[u.ID].ready == nil { userPostsCache.users[u.ID] = postsCacheItem{ready: make(chan struct{})} userPostsCache.Unlock() p, err = app.db.GetUserPosts(u) if err != nil { return err } CachePosts(u.ID, p) } else { userPostsCache.Unlock() <-userPostsCache.users[u.ID].ready p = GetPostsCache(u.ID) } } return impart.WriteSuccess(w, p, http.StatusOK) } func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r) if !reqJSON { return ErrBadRequestedType } p, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { return err } return impart.WriteSuccess(w, p, http.StatusOK) } func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) error { p, err := app.db.GetAnonymousPosts(u, 1) if err != nil { log.Error("unable to fetch anon posts: %v", err) } // nil-out AnonymousPosts slice for easy detection in the template if p != nil && len(*p) == 0 { p = nil } f, err := getSessionFlashes(app, w, r, nil) if err != nil { log.Error("unable to fetch flashes: %v", err) } c, err := app.db.GetPublishableCollections(u, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } silenced, err := app.db.IsUserSilenced(u.ID) if err != nil { if err == ErrUserNotFound { return err } log.Error("view articles: %v", err) } d := struct { *UserPage AnonymousPosts *[]PublicPost Collections *[]Collection Silenced bool }{ UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f), AnonymousPosts: p, Collections: c, Silenced: silenced, } d.UserPage.SetMessaging(u) w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Expires", "Thu, 04 Oct 1990 20:00:00 GMT") showUserPage(w, "articles", d) return nil } func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) error { c, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) return fmt.Errorf("No collections") } f, _ := getSessionFlashes(app, w, r, nil) uc, _ := app.db.GetUserCollectionCount(u.ID) // TODO: handle any errors silenced, err := app.db.IsUserSilenced(u.ID) if err != nil { if err == ErrUserNotFound { return err } log.Error("view collections: %v", err) return fmt.Errorf("view collections: %v", err) } d := struct { *UserPage Collections *[]Collection UsedCollections, TotalCollections int NewBlogsDisabled bool Silenced bool }{ UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f), Collections: c, UsedCollections: int(uc), NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc), Silenced: silenced, } d.UserPage.SetMessaging(u) showUserPage(w, "collections", d) return nil } func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) c, err := app.db.GetCollection(vars["collection"]) if err != nil { return err } - c.hostName = app.cfg.App.Host if c.OwnerID != u.ID { return ErrCollectionNotFound } silenced, err := app.db.IsUserSilenced(u.ID) if err != nil { if err == ErrUserNotFound { return err } log.Error("view edit collection %v", err) return fmt.Errorf("view edit collection: %v", err) } flashes, _ := getSessionFlashes(app, w, r, nil) obj := struct { *UserPage *Collection Silenced bool config.EmailCfg LetterReplyTo string }{ UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes), Collection: c, Silenced: silenced, EmailCfg: app.cfg.Email, } obj.UserPage.CollAlias = c.Alias if obj.EmailCfg.Enabled() { obj.LetterReplyTo = app.db.GetCollectionAttribute(c.ID, collAttrLetterReplyTo) } showUserPage(w, "collection", obj) return nil } func updateSettings(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r) var s userSettings var u *User var sess *sessions.Session var err error if reqJSON { accessToken := r.Header.Get("Authorization") if accessToken == "" { return ErrNoAccessToken } u, err = app.db.GetAPIUser(accessToken) if err != nil { return ErrBadAccessToken } decoder := json.NewDecoder(r.Body) err := decoder.Decode(&s) if err != nil { log.Error("Couldn't parse settings JSON request: %v\n", err) return ErrBadJSON } // Prevent all username updates // TODO: support changing username via JSON API request s.Username = "" } else { u, sess = getUserAndSession(app, r) if u == nil { return ErrNotLoggedIn } err := r.ParseForm() if err != nil { log.Error("Couldn't parse settings form request: %v\n", err) return ErrBadFormData } err = app.formDecoder.Decode(&s, r.PostForm) if err != nil { log.Error("Couldn't decode settings form request: %v\n", err) return ErrBadFormData } } // Do update postUpdateReturn := r.FormValue("return") redirectTo := "/me/settings" if s.IsLogOut { redirectTo += "?logout=1" } else if postUpdateReturn != "" { redirectTo = postUpdateReturn } // Only do updates on values we need if s.Username != "" && s.Username == u.Username { // Username hasn't actually changed; blank it out s.Username = "" } err = app.db.ChangeSettings(app, u, &s) if err != nil { if reqJSON { return err } if err, ok := err.(impart.HTTPError); ok { addSessionFlash(app, w, r, err.Message, nil) } } else { // Successful update. if reqJSON { return impart.WriteSuccess(w, u, http.StatusOK) } if s.IsLogOut { redirectTo = "/me/logout" } else { sess.Values[cookieUserVal] = u.Cookie() addSessionFlash(app, w, r, "Account updated.", nil) } } w.Header().Set("Location", redirectTo) w.WriteHeader(http.StatusFound) return nil } func updatePassphrase(app *App, w http.ResponseWriter, r *http.Request) error { accessToken := r.Header.Get("Authorization") if accessToken == "" { return ErrNoAccessToken } curPass := r.FormValue("current") newPass := r.FormValue("new") // Ensure a new password is given (always required) if newPass == "" { return impart.HTTPError{http.StatusBadRequest, "Provide a new password."} } userID, sudo := app.db.GetUserIDPrivilege(accessToken) if userID == -1 { return ErrBadAccessToken } // Ensure a current password is given if the access token doesn't have sudo // privileges. if !sudo && curPass == "" { return impart.HTTPError{http.StatusBadRequest, "Provide current password."} } // Hash the new password hashedPass, err := auth.HashPass([]byte(newPass)) if err != nil { return impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."} } // Do update err = app.db.ChangePassphrase(userID, sudo, curPass, hashedPass) if err != nil { return err } return impart.WriteSuccess(w, struct{}{}, http.StatusOK) } func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error { var c *Collection var err error vars := mux.Vars(r) alias := vars["collection"] if alias != "" { c, err = app.db.GetCollection(alias) if err != nil { return err } if c.OwnerID != u.ID { return ErrCollectionNotFound } c.hostName = app.cfg.App.Host } topPosts, err := app.db.GetTopPosts(u, alias, c.hostName) if err != nil { log.Error("Unable to get top posts: %v", err) return err } flashes, _ := getSessionFlashes(app, w, r, nil) titleStats := "" if c != nil { titleStats = c.DisplayTitle() + " " } silenced, err := app.db.IsUserSilenced(u.ID) if err != nil { if err == ErrUserNotFound { return err } log.Error("view stats: %v", err) return err } obj := struct { *UserPage VisitsBlog string Collection *Collection TopPosts *[]PublicPost APFollowers int EmailEnabled bool EmailSubscribers int Silenced bool }{ UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes), VisitsBlog: alias, Collection: c, TopPosts: topPosts, EmailEnabled: app.cfg.Email.Enabled(), Silenced: silenced, } obj.UserPage.CollAlias = c.Alias if app.cfg.App.Federation { folls, err := app.db.GetAPFollowers(c) if err != nil { return err } obj.APFollowers = len(*folls) } if obj.EmailEnabled { subs, err := app.db.GetEmailSubscribers(c.ID, true) if err != nil { return err } obj.EmailSubscribers = len(subs) } showUserPage(w, "stats", obj) return nil } func handleViewSubscribers(app *App, u *User, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) c, err := app.db.GetCollection(vars["collection"]) if err != nil { return err } filter := r.FormValue("filter") flashes, _ := getSessionFlashes(app, w, r, nil) obj := struct { *UserPage Collection CollectionNav EmailSubs []*EmailSubscriber Followers *[]RemoteUser Silenced bool Filter string FederationEnabled bool CanEmailSub bool CanAddSubs bool EmailSubsEnabled bool }{ UserPage: NewUserPage(app, r, u, c.DisplayTitle()+" Subscribers", flashes), Collection: CollectionNav{ Collection: c, Path: r.URL.Path, SingleUser: app.cfg.App.SingleUser, }, Silenced: u.IsSilenced(), Filter: filter, FederationEnabled: app.cfg.App.Federation, CanEmailSub: app.cfg.Email.Enabled(), EmailSubsEnabled: c.EmailSubsEnabled(), } obj.Followers, err = app.db.GetAPFollowers(c) if err != nil { return err } obj.EmailSubs, err = app.db.GetEmailSubscribers(c.ID, true) if err != nil { return err } if obj.Filter == "" { // Set permission to add email subscribers //obj.CanAddSubs = app.db.GetUserAttribute(c.OwnerID, userAttrCanAddEmailSubs) == "1" } showUserPage(w, "subscribers", obj) return nil } func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) error { fullUser, err := app.db.GetUserByID(u.ID) if err != nil { if err == ErrUserNotFound { return err } log.Error("Unable to get user for settings: %s", err) return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."} } passIsSet, err := app.db.IsUserPassSet(u.ID) if err != nil { log.Error("Unable to get isUserPassSet for settings: %s", err) return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."} } flashes, _ := getSessionFlashes(app, w, r, nil) enableOauthSlack := app.Config().SlackOauth.ClientID != "" enableOauthWriteAs := app.Config().WriteAsOauth.ClientID != "" enableOauthGitLab := app.Config().GitlabOauth.ClientID != "" enableOauthGeneric := app.Config().GenericOauth.ClientID != "" enableOauthGitea := app.Config().GiteaOauth.ClientID != "" oauthAccounts, err := app.db.GetOauthAccounts(r.Context(), u.ID) if err != nil { log.Error("Unable to get oauth accounts for settings: %s", err) return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."} } for idx, oauthAccount := range oauthAccounts { switch oauthAccount.Provider { case "slack": enableOauthSlack = false case "write.as": enableOauthWriteAs = false case "gitlab": enableOauthGitLab = false case "generic": oauthAccounts[idx].DisplayName = app.Config().GenericOauth.DisplayName oauthAccounts[idx].AllowDisconnect = app.Config().GenericOauth.AllowDisconnect enableOauthGeneric = false case "gitea": enableOauthGitea = false } } displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || enableOauthGeneric || enableOauthGitea || len(oauthAccounts) > 0 obj := struct { *UserPage Email string HasPass bool IsLogOut bool Silenced bool CSRFField template.HTML OauthSection bool OauthAccounts []oauthAccountInfo OauthSlack bool OauthWriteAs bool OauthGitLab bool GitLabDisplayName string OauthGeneric bool OauthGenericDisplayName string OauthGitea bool GiteaDisplayName string }{ UserPage: NewUserPage(app, r, u, "Account Settings", flashes), Email: fullUser.EmailClear(app.keys), HasPass: passIsSet, IsLogOut: r.FormValue("logout") == "1", Silenced: fullUser.IsSilenced(), CSRFField: csrf.TemplateField(r), OauthSection: displayOauthSection, OauthAccounts: oauthAccounts, OauthSlack: enableOauthSlack, OauthWriteAs: enableOauthWriteAs, OauthGitLab: enableOauthGitLab, GitLabDisplayName: config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName), OauthGeneric: enableOauthGeneric, OauthGenericDisplayName: config.OrDefaultString(app.Config().GenericOauth.DisplayName, genericOauthDisplayName), OauthGitea: enableOauthGitea, GiteaDisplayName: config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName), } showUserPage(w, "settings", obj) return nil } func viewResetPassword(app *App, w http.ResponseWriter, r *http.Request) error { token := r.FormValue("t") resetting := false var userID int64 = 0 if token != "" { // Show new password page userID = app.db.GetUserFromPasswordReset(token) if userID == 0 { return impart.HTTPError{http.StatusNotFound, ""} } resetting = true } if r.Method == http.MethodPost { newPass := r.FormValue("new-pass") if newPass == "" { // Send password reset email return handleResetPasswordInit(app, w, r) } // Do actual password reset // Assumes token has been validated above err := doAutomatedPasswordChange(app, userID, newPass) if err != nil { return err } err = app.db.ConsumePasswordResetToken(token) if err != nil { log.Error("Couldn't consume token %s for user %d!!! %s", token, userID, err) } addSessionFlash(app, w, r, "Your password was reset. Now you can log in below.", nil) return impart.HTTPError{http.StatusFound, "/login"} } f, _ := getSessionFlashes(app, w, r, nil) // Show reset password page d := struct { page.StaticPage Flashes []string EmailEnabled bool CSRFField template.HTML Token string IsResetting bool IsSent bool }{ StaticPage: pageForReq(app, r), Flashes: f, EmailEnabled: app.cfg.Email.Enabled(), CSRFField: csrf.TemplateField(r), Token: token, IsResetting: resetting, IsSent: r.FormValue("sent") == "1", } err := pages["reset.tmpl"].ExecuteTemplate(w, "base", d) if err != nil { log.Error("Unable to render password reset page: %v", err) return err } return err } func doAutomatedPasswordChange(app *App, userID int64, newPass string) error { // Do password reset hashedPass, err := auth.HashPass([]byte(newPass)) if err != nil { return impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."} } // Do update err = app.db.ChangePassphrase(userID, true, "", hashedPass) if err != nil { return err } return nil } func handleResetPasswordInit(app *App, w http.ResponseWriter, r *http.Request) error { returnLoc := impart.HTTPError{http.StatusFound, "/reset"} if !app.cfg.Email.Enabled() { // Email isn't configured, so there's nothing to do; send back to the reset form, where they'll get an explanation return returnLoc } ip := spam.GetIP(r) alias := r.FormValue("alias") u, err := app.db.GetUserForAuth(alias) if err != nil { if strings.IndexAny(alias, "@") > 0 { addSessionFlash(app, w, r, ErrUserNotFoundEmail.Message, nil) return returnLoc } addSessionFlash(app, w, r, ErrUserNotFound.Message, nil) return returnLoc } if u.IsAdmin() { // Prevent any reset emails on admin accounts log.Error("Admin reset attempt", `Someone just tried to reset the password for an admin (ID %d - %s). IP address: %s`, u.ID, u.Username, ip) return returnLoc } if u.Email.String == "" { - err := impart.HTTPError{http.StatusPreconditionFailed, "User doesn't have an email address. Please contact us (" + app.cfg.App.Host + "/contact) to reset your password."} + err := impart.HTTPError{http.StatusPreconditionFailed, "User doesn't have an email address. Please contact us (" + app.cfg.App.AbsoluteURL("/contact") + ") to reset your password."} addSessionFlash(app, w, r, err.Message, nil) return returnLoc } if isSet, _ := app.db.IsUserPassSet(u.ID); !isSet { err = loginViaEmail(app, u.Username, "/me/settings") if err != nil { return err } addSessionFlash(app, w, r, "We've emailed you a link to log in with.", nil) return returnLoc } token, err := app.db.CreatePasswordResetToken(u.ID) if err != nil { log.Error("Error resetting password: %s", err) addSessionFlash(app, w, r, ErrInternalGeneral.Message, nil) return returnLoc } err = emailPasswordReset(app, u.EmailClear(app.keys), token) if err != nil { log.Error("Error emailing password reset: %s", err) addSessionFlash(app, w, r, ErrInternalGeneral.Message, nil) return returnLoc } addSessionFlash(app, w, r, "We sent an email to the address associated with this account.", nil) returnLoc.Message += "?sent=1" return returnLoc } func emailPasswordReset(app *App, toEmail, token string) error { // Send email mlr, err := mailer.New(app.cfg.Email) if err != nil { return err } footerPara := "Didn't request this password reset? Your account is still safe, and you can safely ignore this email." plainMsg := fmt.Sprintf("We received a request to reset your password on %s. Please click the following link to continue (or copy and paste it into your browser): %s/reset?t=%s\n\n%s", app.cfg.App.SiteName, app.cfg.App.Host, token, footerPara) m, err := mlr.NewMessage(app.cfg.App.SiteName+" ", "Reset Your "+app.cfg.App.SiteName+" Password", plainMsg, fmt.Sprintf("<%s>", toEmail)) if err != nil { return err } m.AddTag("Password Reset") m.SetHTML(fmt.Sprintf(`

%s

We received a request to reset your password on %s. Please click the following link to continue:

Reset your password

%s

`, app.cfg.App.Host, app.cfg.App.SiteName, app.cfg.App.SiteName, app.cfg.App.Host, token, footerPara)) return mlr.Send(m) } func loginViaEmail(app *App, alias, redirectTo string) error { if !app.cfg.Email.Enabled() { return fmt.Errorf("EMAIL ISN'T CONFIGURED on this server") } // Make sure user has added an email // TODO: create a new func to just get user's email; "ForAuth" doesn't match here u, _ := app.db.GetUserForAuth(alias) if u == nil { if strings.IndexAny(alias, "@") > 0 { return ErrUserNotFoundEmail } return ErrUserNotFound } if u.Email.String == "" { return impart.HTTPError{http.StatusPreconditionFailed, "User doesn't have an email address. Log in with password, instead."} } // Generate one-time login token t, err := app.db.GetTemporaryOneTimeAccessToken(u.ID, 60*15, true) if err != nil { log.Error("Unable to generate token for email login: %s", err) return impart.HTTPError{http.StatusInternalServerError, "Unable to generate token."} } // Send email mlr, err := mailer.New(app.cfg.Email) if err != nil { return err } toEmail := u.EmailClear(app.keys) footerPara := "This link will only work once and expires in 15 minutes. Didn't ask us to log in? You can safely ignore this email." plainMsg := fmt.Sprintf("Log in to %s here: %s/login?to=%s&with=%s\n\n%s", app.cfg.App.SiteName, app.cfg.App.Host, redirectTo, t, footerPara) m, err := mlr.NewMessage(app.cfg.App.SiteName+" ", "Log in to "+app.cfg.App.SiteName, plainMsg, fmt.Sprintf("<%s>", toEmail)) if err != nil { return err } m.AddTag("Email Login") m.SetHTML(fmt.Sprintf(`

%s

Log in to %s here.

%s

`, app.cfg.App.Host, app.cfg.App.SiteName, app.cfg.App.Host, redirectTo, t, app.cfg.App.SiteName, footerPara)) return mlr.Send(m) } func saveTempInfo(app *App, key, val string, r *http.Request, w http.ResponseWriter) error { session, err := app.sessionStore.Get(r, "t") if err != nil { return ErrInternalCookieSession } session.Values[key] = val err = session.Save(r, w) if err != nil { log.Error("Couldn't saveTempInfo for key-val (%s:%s): %v", key, val, err) } return err } func getTempInfo(app *App, key string, r *http.Request, w http.ResponseWriter) string { session, err := app.sessionStore.Get(r, "t") if err != nil { return "" } // Get the information var s = "" var ok bool if s, ok = session.Values[key].(string); !ok { return "" } // Delete cookie session.Options.MaxAge = -1 err = session.Save(r, w) if err != nil { log.Error("Couldn't erase temp data for key %s: %v", key, err) } // Return value return s } func handleUserDelete(app *App, u *User, w http.ResponseWriter, r *http.Request) error { if !app.cfg.App.OpenDeletion { return impart.HTTPError{http.StatusForbidden, "Open account deletion is disabled on this instance."} } confirmUsername := r.PostFormValue("confirm-username") if u.Username != confirmUsername { return impart.HTTPError{http.StatusBadRequest, "Confirmation username must match your username exactly."} } // Check for account deletion safeguards in place if u.IsAdmin() { return impart.HTTPError{http.StatusForbidden, "Cannot delete admin."} } err := app.db.DeleteAccount(u.ID) if err != nil { log.Error("user delete account: %v", err) return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not delete account: %v", err)} } // FIXME: This doesn't ever appear to the user, as (I believe) the value is erased when the session cookie is reset _ = addSessionFlash(app, w, r, "Thanks for writing with us! You account was deleted successfully.", nil) return impart.HTTPError{http.StatusFound, "/me/logout"} } func removeOauth(app *App, u *User, w http.ResponseWriter, r *http.Request) error { provider := r.FormValue("provider") clientID := r.FormValue("client_id") remoteUserID := r.FormValue("remote_user_id") err := app.db.RemoveOauth(r.Context(), u.ID, provider, clientID, remoteUserID) if err != nil { return impart.HTTPError{Status: http.StatusInternalServerError, Message: err.Error()} } return impart.HTTPError{Status: http.StatusFound, Message: "/me/settings"} } func prepareUserEmail(input string, emailKey []byte) zero.String { email := zero.NewString("", input != "") if len(input) > 0 { encEmail, err := data.Encrypt(emailKey, input) if err != nil { log.Error("Unable to encrypt email: %s\n", err) } else { email.String = string(encEmail) } } return email } diff --git a/activitypub.go b/activitypub.go index 14eeeff..37880a3 100644 --- a/activitypub.go +++ b/activitypub.go @@ -1,1245 +1,1247 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "bytes" "crypto/sha256" "database/sql" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/http/httputil" "net/url" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/gorilla/mux" "github.com/writeas/activity/streams" "github.com/writeas/activityserve" "github.com/writeas/httpsig" "github.com/writeas/impart" "github.com/writeas/web-core/activitypub" "github.com/writeas/web-core/activitystreams" "github.com/writeas/web-core/id" "github.com/writeas/web-core/log" "github.com/writeas/web-core/silobridge" ) const ( // TODO: delete. don't use this! apCustomHandleDefault = "blog" apCacheTime = time.Minute ) var ( - apCollectionPostIRIRegex = regexp.MustCompile("/api/collections/([a-z0-9\\-]+)/posts/([a-z0-9\\-]+)$") - apDraftPostIRIRegex = regexp.MustCompile("/api/posts/([a-z0-9\\-]+)$") + apCollectionPostIRIRegex = regexp.MustCompile("^/api/collections/([a-z0-9\\-]+)/posts/([a-z0-9\\-]+)$") + apDraftPostIRIRegex = regexp.MustCompile("^/api/posts/([a-z0-9\\-]+)$") ) var instanceColl *Collection func initActivityPub(app *App) { - ur, _ := url.Parse(app.cfg.App.Host) + ur, _ := url.Parse(app.cfg.App.AbsoluteHost()) instanceColl = &Collection{ ID: 0, Alias: ur.Host, Title: ur.Host, db: app.db, - hostName: app.cfg.App.Host, + hostName: app.cfg.App.AbsoluteHost(), } } type RemoteUser struct { ID int64 ActorID string Inbox string SharedInbox string URL string Handle string Created time.Time } func (ru *RemoteUser) CreatedFriendly() string { return ru.Created.Format("January 2, 2006") } func (ru *RemoteUser) EstimatedHandle() string { if ru.Handle != "" { return ru.Handle } username := filepath.Base(ru.ActorID) host, _ := url.Parse(ru.ActorID) return username + "@" + host.Host } func (ru *RemoteUser) AsPerson() *activitystreams.Person { return &activitystreams.Person{ BaseObject: activitystreams.BaseObject{ Type: "Person", Context: []interface{}{ activitystreams.Namespace, }, ID: ru.ActorID, }, Inbox: ru.Inbox, Endpoints: activitystreams.Endpoints{ SharedInbox: ru.SharedInbox, }, } } func activityPubClient() *http.Client { return &http.Client{ Timeout: 15 * time.Second, } } func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Request) error { w.Header().Set("Server", serverSoftware) vars := mux.Vars(r) alias := vars["alias"] if alias == "" { alias = filepath.Base(r.RequestURI) } // TODO: enforce visibility // Get base Collection data var c *Collection var err error if alias == r.Host { c = instanceColl } else if app.cfg.App.SingleUser { c, err = app.db.GetCollectionByID(1) } else { c, err = app.db.GetCollection(alias) } if err != nil { return err } - c.hostName = app.cfg.App.Host + c.hostName = app.cfg.App.AbsoluteHost() if !c.IsInstanceColl() { silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("fetch collection activities: %v", err) return ErrInternalGeneral } if silenced { return ErrCollectionNotFound } } p := c.PersonObject() setCacheControl(w, apCacheTime) return impart.RenderActivityJSON(w, p, http.StatusOK) } func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Request) error { w.Header().Set("Server", serverSoftware) vars := mux.Vars(r) alias := vars["alias"] // TODO: enforce visibility // Get base Collection data var c *Collection var err error if app.cfg.App.SingleUser { c, err = app.db.GetCollectionByID(1) } else { c, err = app.db.GetCollection(alias) } if err != nil { return err } silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("fetch collection outbox: %v", err) return ErrInternalGeneral } if silenced { return ErrCollectionNotFound } - c.hostName = app.cfg.App.Host + c.hostName = app.cfg.App.AbsoluteHost() if app.cfg.App.SingleUser { if alias != c.Alias { return ErrCollectionNotFound } } res := &CollectionObj{Collection: *c} app.db.GetPostsCount(res, false) accountRoot := c.FederatedAccount() page := r.FormValue("page") p, err := strconv.Atoi(page) if err != nil || p < 1 { // Return outbox oc := activitystreams.NewOrderedCollection(accountRoot, "outbox", res.TotalPosts) return impart.RenderActivityJSON(w, oc, http.StatusOK) } // Return outbox page ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "outbox", res.TotalPosts, p) ocp.OrderedItems = []interface{}{} posts, err := app.db.GetPosts(app.cfg, c, p, false, true, false, "") for _, pp := range *posts { pp.Collection = res o := pp.ActivityObject(app) a := activitystreams.NewCreateActivity(o) a.Context = nil ocp.OrderedItems = append(ocp.OrderedItems, *a) } setCacheControl(w, apCacheTime) return impart.RenderActivityJSON(w, ocp, http.StatusOK) } func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Request) error { w.Header().Set("Server", serverSoftware) vars := mux.Vars(r) alias := vars["alias"] // TODO: enforce visibility // Get base Collection data var c *Collection var err error if app.cfg.App.SingleUser { c, err = app.db.GetCollectionByID(1) } else { c, err = app.db.GetCollection(alias) } if err != nil { return err } silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("fetch collection followers: %v", err) return ErrInternalGeneral } if silenced { return ErrCollectionNotFound } - c.hostName = app.cfg.App.Host + c.hostName = app.cfg.App.AbsoluteHost() accountRoot := c.FederatedAccount() folls, err := app.db.GetAPFollowers(c) if err != nil { return err } page := r.FormValue("page") p, err := strconv.Atoi(page) if err != nil || p < 1 { // Return outbox oc := activitystreams.NewOrderedCollection(accountRoot, "followers", len(*folls)) return impart.RenderActivityJSON(w, oc, http.StatusOK) } // Return outbox page ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "followers", len(*folls), p) ocp.OrderedItems = []interface{}{} /* for _, f := range *folls { ocp.OrderedItems = append(ocp.OrderedItems, f.ActorID) } */ setCacheControl(w, apCacheTime) return impart.RenderActivityJSON(w, ocp, http.StatusOK) } func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Request) error { w.Header().Set("Server", serverSoftware) vars := mux.Vars(r) alias := vars["alias"] // TODO: enforce visibility // Get base Collection data var c *Collection var err error if app.cfg.App.SingleUser { c, err = app.db.GetCollectionByID(1) } else { c, err = app.db.GetCollection(alias) } if err != nil { return err } silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("fetch collection following: %v", err) return ErrInternalGeneral } if silenced { return ErrCollectionNotFound } - c.hostName = app.cfg.App.Host + c.hostName = app.cfg.App.AbsoluteHost() accountRoot := c.FederatedAccount() page := r.FormValue("page") p, err := strconv.Atoi(page) if err != nil || p < 1 { // Return outbox oc := activitystreams.NewOrderedCollection(accountRoot, "following", 0) return impart.RenderActivityJSON(w, oc, http.StatusOK) } // Return outbox page ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "following", 0, p) ocp.OrderedItems = []interface{}{} setCacheControl(w, apCacheTime) return impart.RenderActivityJSON(w, ocp, http.StatusOK) } func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request) error { w.Header().Set("Server", serverSoftware) vars := mux.Vars(r) alias := vars["alias"] var c *Collection var err error if app.cfg.App.SingleUser { c, err = app.db.GetCollectionByID(1) } else { c, err = app.db.GetCollection(alias) } if err != nil { // TODO: return Reject? return err } silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("fetch collection inbox: %v", err) return ErrInternalGeneral } if silenced { return ErrCollectionNotFound } - c.hostName = app.cfg.App.Host + c.hostName = app.cfg.App.AbsoluteHost() if debugging { dump, err := httputil.DumpRequest(r, true) if err != nil { log.Error("Can't dump: %v", err) } else { log.Info("Rec'd! %q", dump) } } - // Only call impart.RenderActivityJSON here if NO callback has already written a response. - // Track whether a callback has written a response - var responseWritten bool - - // Read raw body for debugging before decoding - var rawBody bytes.Buffer - tee := io.TeeReader(r.Body, &rawBody) - - var m map[string]any - if err := json.NewDecoder(tee).Decode(&m); err != nil { - log.Error("Failed decoding JSON: %v", err) - log.Error("Raw body: %s", rawBody.String()) + var m map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&m); err != nil { return err } - if debugging { - log.Info("Decoded JSON: %v", m) - } a := streams.NewAccept() p := c.PersonObject() var to *url.URL var isFollow, isUnfollow, isLike, isUnlike bool var likePostID, unlikePostID string fullActor := &activitystreams.Person{} var remoteUser *RemoteUser res := &streams.Resolver{ LikeCallback: func(l *streams.Like) error { isLike = true // 1) Use the Like concrete type here // 2) Errors are propagated to res.Deserialize call below m["@context"] = []string{activitystreams.Namespace} b, _ := json.Marshal(m) if debugging { log.Info("Like: %s", b) } _, likeID := l.GetId() if likeID == nil { log.Error("Didn't resolve Like ID") } if p := l.HasObject(0); p == streams.NoPresence { return fmt.Errorf("no object for Like activity at index 0") } obj := l.Raw().GetObjectIRI(0) /* // TODO: handle this more robustly l.ResolveObject(&streams.Resolver{ LinkCallback: func(link *streams.Link) error { return nil }, }, 0) */ if obj == nil { return fmt.Errorf("didn't get ObjectIRI to Like") } likePostID, err = parsePostIDFromURL(app, obj) if err != nil { return err } // Finally, get actor information _, from := l.GetActor(0) if from == nil { return fmt.Errorf("No valid actor string") } fullActor, remoteUser, err = getActor(app, from.String()) if err != nil { return err } - responseWritten = true return nil }, FollowCallback: func(f *streams.Follow) error { isFollow = true // 1) Use the Follow concrete type here // 2) Errors are propagated to res.Deserialize call below m["@context"] = []string{activitystreams.Namespace} b, _ := json.Marshal(m) if debugging { log.Info("Follow: %s", b) } _, followID := f.GetId() if followID == nil { log.Error("Didn't resolve follow ID") } else { aID := c.FederatedAccount() + "#accept-" + id.GenerateFriendlyRandomString(20) acceptID, err := url.Parse(aID) if err != nil { log.Error("Couldn't parse generated Accept URL '%s': %v", aID, err) } a.SetId(acceptID) } a.AppendObject(f.Raw()) _, to = f.GetActor(0) obj := f.Raw().GetObjectIRI(0) if obj == nil { if debugging { log.Error("GetObjectIRI on Follow for actor is empty; trying object") } ao := f.Raw().GetObject(0) if ao == nil { log.Error("Fell back to GetObject and none parsed, so no actor ID! Follow request probably FAILED!") } else { obj = ao.GetId() } } a.AppendActor(obj) // First get actor information if to == nil { return fmt.Errorf("No valid `to` string") } fullActor, remoteUser, err = getActor(app, to.String()) if err != nil { return err } - responseWritten = true return impart.RenderActivityJSON(w, m, http.StatusOK) }, UndoCallback: func(u *streams.Undo) error { m["@context"] = []string{activitystreams.Namespace} b, _ := json.Marshal(m) if debugging { log.Info("Undo: %s", b) } a.AppendObject(u.Raw()) // Check type -- we handle Undo:Like and Undo:Follow _, err := u.ResolveObject(&streams.Resolver{ LikeCallback: func(like *streams.Like) error { isUnlike = true _, from := like.GetActor(0) obj := like.Raw().GetObjectIRI(0) if obj == nil { return fmt.Errorf("didn't get ObjectIRI for Undo Like") } unlikePostID, err = parsePostIDFromURL(app, obj) if err != nil { return err } fullActor, remoteUser, err = getActor(app, from.String()) if err != nil { return err } return nil }, // TODO: add FollowCallback for more robust handling }, 0) if err != nil { return err } if isUnlike { return nil } isUnfollow = true _, to = u.GetActor(0) // TODO: get actor from object.object, not object obj := u.Raw().GetObjectIRI(0) a.AppendActor(obj) if to != nil { // Populate fullActor from DB? remoteUser, err = getRemoteUser(app, to.String()) if err != nil { if iErr, ok := err.(*impart.HTTPError); ok { if iErr.Status == http.StatusNotFound { log.Error("No remoteuser info for Undo event!") } } return err } else { fullActor = remoteUser.AsPerson() } } else { log.Error("No to on Undo!") } - responseWritten = true return impart.RenderActivityJSON(w, m, http.StatusOK) }, - DeleteCallback: func(d *streams.Delete) error { - if debugging { - b, _ := json.Marshal(m) - log.Info("Delete: %s", b) - } - impart.RenderActivityJSON(w, m, http.StatusOK) - responseWritten = true - return nil - }, } if err := res.Deserialize(m); err != nil { // 3) Any errors from #2 can be handled, or the payload is an unknown type. - log.Error("Unable to resolve Activity: %v", err) + log.Error("Unable to resolve Follow: %v", err) if debugging { log.Error("Map: %s", m) } - if t, ok := m["type"]; ok { - log.Error("Unhandled activity type: %v", t) - } - impart.RenderActivityJSON(w, "", http.StatusOK) - return nil + return err } // Handle synchronous activities if isLike { t, err := app.db.Begin() if err != nil { log.Error("Unable to start transaction: %v", err) return fmt.Errorf("unable to start transaction: %v", err) } var remoteUserID int64 if remoteUser != nil { remoteUserID = remoteUser.ID } else { remoteUserID, err = apAddRemoteUser(app, t, fullActor) } // Add like _, err = t.Exec("INSERT INTO remote_likes (post_id, remote_user_id, created) VALUES (?, ?, "+app.db.now()+")", likePostID, remoteUserID) if err != nil { if !app.db.isDuplicateKeyErr(err) { t.Rollback() log.Error("Couldn't add like in DB: %v\n", err) return fmt.Errorf("Couldn't add like in DB: %v", err) } else { t.Rollback() log.Error("Couldn't add like in DB: %v\n", err) return fmt.Errorf("Couldn't add like in DB: %v", err) } } err = t.Commit() if err != nil { t.Rollback() log.Error("Rolling back after Commit(): %v\n", err) return fmt.Errorf("Rolling back after Commit(): %v\n", err) } if debugging { log.Info("Successfully liked post %s by remote user %s", likePostID, remoteUser.URL) } - impart.RenderActivityJSON(w, "", http.StatusOK) - return nil + return impart.RenderActivityJSON(w, "", http.StatusOK) } else if isUnlike { t, err := app.db.Begin() if err != nil { log.Error("Unable to start transaction: %v", err) return fmt.Errorf("unable to start transaction: %v", err) } var remoteUserID int64 if remoteUser != nil { remoteUserID = remoteUser.ID } else { remoteUserID, err = apAddRemoteUser(app, t, fullActor) } // Remove like _, err = t.Exec("DELETE FROM remote_likes WHERE post_id = ? AND remote_user_id = ?", unlikePostID, remoteUserID) if err != nil { t.Rollback() log.Error("Couldn't delete Like from DB: %v\n", err) return fmt.Errorf("Couldn't delete Like from DB: %v", err) } err = t.Commit() if err != nil { t.Rollback() log.Error("Rolling back after Commit(): %v\n", err) return fmt.Errorf("Rolling back after Commit(): %v\n", err) } if debugging { log.Info("Successfully un-liked post %s by remote user %s", unlikePostID, remoteUser.URL) } - impart.RenderActivityJSON(w, "", http.StatusOK) - return nil + return impart.RenderActivityJSON(w, "", http.StatusOK) } go func() { if to == nil { if debugging { - log.Info("No `to` value: likely not needed for this activity type.") + log.Error("No `to` value!") } return } time.Sleep(2 * time.Second) am, err := a.Serialize() if err != nil { log.Error("Unable to serialize Accept: %v", err) return } am["@context"] = []string{activitystreams.Namespace} - if debugging { - logOutgoingActivity("Accept", am) - } err = makeActivityPost(app.cfg.App.Host, p, fullActor.Inbox, am) if err != nil { log.Error("Unable to make activity POST: %v", err) return } if isFollow { t, err := app.db.Begin() if err != nil { log.Error("Unable to start transaction: %v", err) return } var followerID int64 if remoteUser != nil { followerID = remoteUser.ID } else { // TODO: use apAddRemoteUser() here, instead! // Add follower locally, since it wasn't found before res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox, url) VALUES (?, ?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox, fullActor.URL) if err != nil { // if duplicate key, res will be nil and panic on // res.LastInsertId below t.Rollback() log.Error("Couldn't add new remoteuser in DB: %v\n", err) return } followerID, err = res.LastInsertId() if err != nil { t.Rollback() log.Error("no lastinsertid for followers, rolling back: %v", err) return } // Add in key _, err = t.Exec("INSERT INTO remoteuserkeys (id, remote_user_id, public_key) VALUES (?, ?, ?)", fullActor.PublicKey.ID, followerID, fullActor.PublicKey.PublicKeyPEM) if err != nil { if !app.db.isDuplicateKeyErr(err) { t.Rollback() log.Error("Couldn't add follower keys in DB: %v\n", err) return } } } // Add follow _, err = t.Exec("INSERT INTO remotefollows (collection_id, remote_user_id, created) VALUES (?, ?, "+app.db.now()+")", c.ID, followerID) if err != nil { if !app.db.isDuplicateKeyErr(err) { t.Rollback() log.Error("Couldn't add follower in DB: %v\n", err) return } } err = t.Commit() if err != nil { t.Rollback() log.Error("Rolling back after Commit(): %v\n", err) return } } else if isUnfollow { // Remove follower locally _, err = app.db.Exec("DELETE FROM remotefollows WHERE collection_id = ? AND remote_user_id = (SELECT id FROM remoteusers WHERE actor_id = ?)", c.ID, to.String()) if err != nil { log.Error("Couldn't remove follower from DB: %v\n", err) } } }() - if !responseWritten { - if debugging { - log.Info("Received unhandled activity type, returning OK") - } - impart.RenderActivityJSON(w, "", http.StatusOK) - } - return nil } func makeActivityPost(hostName string, p *activitystreams.Person, url string, m interface{}) error { - if url == "" { - log.Error("Target POST URL is empty! Person: %+v, Activity: %+v", p, m) - return fmt.Errorf("target POST URL is empty") - } - log.Info("POST %s", url) b, err := json.Marshal(m) if err != nil { return err } r, _ := http.NewRequest("POST", url, bytes.NewBuffer(b)) r.Header.Add("Content-Type", "application/activity+json") r.Header.Set("User-Agent", ServerUserAgent(hostName)) + r.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat)) h := sha256.New() h.Write(b) r.Header.Add("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(h.Sum(nil))) // Sign using the 'Signature' header privKey, err := activitypub.DecodePrivateKey(p.GetPrivKey()) if err != nil { return err } signer := httpsig.NewSigner(p.PublicKey.ID, privKey, httpsig.RSASHA256, []string{"(request-target)", "date", "host", "digest"}) err = signer.SignSigHeader(r) if err != nil { log.Error("Can't sign: %v", err) } if debugging { dump, err := httputil.DumpRequestOut(r, true) if err != nil { log.Error("Can't dump: %v", err) } else { log.Info("%s", dump) } } resp, err := activityPubClient().Do(r) if err != nil { return err } if resp != nil && resp.Body != nil { defer resp.Body.Close() } body, err := io.ReadAll(resp.Body) if err != nil { return err } - if debugging { + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + log.Error("Response status %s from %s: %s", resp.Status, url, body) + } else if debugging { log.Info("Status : %s", resp.Status) log.Info("Response: %s", body) } return nil } func resolveIRI(hostName, url string) ([]byte, error) { log.Info("GET %s", url) r, _ := http.NewRequest("GET", url, nil) r.Header.Add("Accept", "application/activity+json") r.Header.Set("User-Agent", ServerUserAgent(hostName)) p := instanceColl.PersonObject() h := sha256.New() h.Write([]byte{}) r.Header.Add("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(h.Sum(nil))) // Sign using the 'Signature' header privKey, err := activitypub.DecodePrivateKey(p.GetPrivKey()) if err != nil { return nil, err } signer := httpsig.NewSigner(p.PublicKey.ID, privKey, httpsig.RSASHA256, []string{"(request-target)", "date", "host", "digest"}) err = signer.SignSigHeader(r) if err != nil { log.Error("Can't sign: %v", err) } if debugging { dump, err := httputil.DumpRequestOut(r, true) if err != nil { log.Error("Can't dump: %v", err) } else { log.Info("%s", dump) } } resp, err := activityPubClient().Do(r) if err != nil { return nil, err } if resp != nil && resp.Body != nil { defer resp.Body.Close() } body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if debugging { log.Info("Status : %s", resp.Status) log.Info("Response: %s", body) } return body, nil } +// federateActor sends an Update{Person} activity to all followers of the +// given collection, notifying them of profile changes (bio, name, icon, etc.). +func federateActor(app *App, c *Collection) error { + if app.cfg.App.Private { + return nil + } + if c.Visibility == CollPrivate || c.Visibility == CollProtected { + return nil + } + + c.hostName = app.cfg.App.Host + actor := c.PersonObject() + + followers, err := app.db.GetAPFollowers(c) + if err != nil { + return err + } + + inboxes := map[string][]string{} + for _, f := range *followers { + inbox := f.SharedInbox + if inbox == "" { + inbox = f.Inbox + } + if _, ok := inboxes[inbox]; ok { + inboxes[inbox] = append(inboxes[inbox], f.ActorID) + } else { + inboxes[inbox] = []string{f.ActorID} + } + } + + for si := range inboxes { + // Build the Update{Person} activity as a plain map so the full + // actor object serialises correctly as the activity's object field. + activity := map[string]interface{}{ + "@context": activitystreams.Namespace, + "id": actor.ID + "#Update", + "type": "Update", + "actor": actor.ID, + "to": []string{"https://www.w3.org/ns/activitystreams#Public"}, + "object": actor, + } + err = makeActivityPost(app.cfg.App.Host, actor, si, activity) + if err != nil { + log.Error("Couldn't federate actor update! %v", err) + } + } + return nil +} + func deleteFederatedPost(app *App, p *PublicPost, collID int64) error { if debugging { log.Info("Deleting federated post!") } p.Collection.hostName = app.cfg.App.Host actor := p.Collection.PersonObject(collID) na := p.ActivityObject(app) // Add followers p.Collection.ID = collID followers, err := app.db.GetAPFollowers(&p.Collection.Collection) if err != nil { log.Error("Couldn't delete post (get followers)! %v", err) return err } inboxes := map[string][]string{} for _, f := range *followers { inbox := f.SharedInbox if inbox == "" { inbox = f.Inbox } if _, ok := inboxes[inbox]; ok { inboxes[inbox] = append(inboxes[inbox], f.ActorID) } else { inboxes[inbox] = []string{f.ActorID} } } for si, instFolls := range inboxes { na.CC = []string{} na.CC = append(na.CC, instFolls...) da := activitystreams.NewDeleteActivity(na) // Make the ID unique to ensure it works in Pleroma // See: https://git.pleroma.social/pleroma/pleroma/issues/1481 da.ID += "#Delete" err = makeActivityPost(app.cfg.App.Host, actor, si, da) if err != nil { log.Error("Couldn't delete post! %v", err) } } return nil } func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error { // If app is private, do not federate if app.cfg.App.Private { return nil } // Do not federate posts from private or protected blogs if p.Collection.Visibility == CollPrivate || p.Collection.Visibility == CollProtected { return nil } if debugging { if isUpdate { log.Info("Federating updated post!") } else { log.Info("Federating new post!") } } actor := p.Collection.PersonObject(collID) na := p.ActivityObject(app) // Add followers p.Collection.ID = collID followers, err := app.db.GetAPFollowers(&p.Collection.Collection) if err != nil { log.Error("Couldn't post! %v", err) return err } log.Info("Followers for %d: %+v", collID, followers) inboxes := map[string][]string{} for _, f := range *followers { inbox := f.SharedInbox if inbox == "" { inbox = f.Inbox } if _, ok := inboxes[inbox]; ok { // check if we're already sending to this shared inbox inboxes[inbox] = append(inboxes[inbox], f.ActorID) } else { // add the new shared inbox to the list inboxes[inbox] = []string{f.ActorID} } } var activity *activitystreams.Activity // for each one of the shared inboxes for si, instFolls := range inboxes { // add all followers from that instance // to the CC field na.CC = []string{} na.CC = append(na.CC, instFolls...) // create a new "Create" activity // with our article as object - label := "Create" if isUpdate { - label = "Update" na.Updated = &p.Updated activity = activitystreams.NewUpdateActivity(na) } else { activity = activitystreams.NewCreateActivity(na) activity.To = na.To activity.CC = na.CC } // and post it to that sharedInbox - if debugging { - logOutgoingActivity(label, activity) - } err = makeActivityPost(app.cfg.App.Host, actor, si, activity) if err != nil { log.Error("Couldn't post! %v", err) } } // re-create the object so that the CC list gets reset and has // the mentioned users. This might seem wasteful but the code is // cleaner than adding the mentioned users to CC here instead of // in p.ActivityObject() na = p.ActivityObject(app) for _, tag := range na.Tag { if tag.Type == "Mention" { activity = activitystreams.NewCreateActivity(na) activity.To = na.To activity.CC = na.CC // This here might be redundant in some cases as we might have already // sent this to the sharedInbox of this instance above, but we need too // much logic to catch this at the expense of the odd extra request. // I don't believe we'd ever have too many mentions in a single post that this // could become a burden. remoteUser, err := getRemoteUser(app, tag.HRef) if err != nil { log.Error("Unable to find remote user %s. Skipping: %v", tag.HRef, err) continue } err = makeActivityPost(app.cfg.App.Host, actor, remoteUser.Inbox, activity) if err != nil { log.Error("Couldn't post! %v", err) } } } return nil } func getRemoteUser(app *App, actorID string) (*RemoteUser, error) { u := RemoteUser{ActorID: actorID} var urlVal, handle sql.NullString err := app.db.QueryRow("SELECT id, inbox, shared_inbox, url, handle FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox, &urlVal, &handle) switch { case err == sql.ErrNoRows: return nil, impart.HTTPError{http.StatusNotFound, "No remote user with that ID."} case err != nil: log.Error("Couldn't get remote user %s: %v", actorID, err) return nil, err } u.URL = urlVal.String u.Handle = handle.String return &u, nil } // getRemoteUserFromHandle retrieves the profile page of a remote user // from the @user@server.tld handle func getRemoteUserFromHandle(app *App, handle string) (*RemoteUser, error) { u := RemoteUser{Handle: handle} var urlVal sql.NullString err := app.db.QueryRow("SELECT id, actor_id, inbox, shared_inbox, url FROM remoteusers WHERE handle = ?", handle).Scan(&u.ID, &u.ActorID, &u.Inbox, &u.SharedInbox, &urlVal) switch { case err == sql.ErrNoRows: return nil, ErrRemoteUserNotFound case err != nil: log.Error("Couldn't get remote user %s: %v", handle, err) return nil, err } u.URL = urlVal.String return &u, nil } // getRemoteUserFromURL retrieves a RemoteUser from their public profile URL. func getRemoteUserFromURL(app *App, urlStr string) (*RemoteUser, error) { u := RemoteUser{URL: urlStr} var urlVal, handle sql.NullString err := app.db.QueryRow("SELECT id, actor_id, inbox, shared_inbox, url, handle FROM remoteusers WHERE url = ?", urlStr).Scan(&u.ID, &u.ActorID, &u.Inbox, &u.SharedInbox, &urlVal, &handle) switch { case err == sql.ErrNoRows: return nil, ErrRemoteUserNotFound case err != nil: log.Error("Couldn't get remote user from URL %s: %v", urlStr, err) return nil, err } u.URL = urlVal.String u.Handle = handle.String return &u, nil } func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, error) { log.Info("Fetching actor %s locally", actorIRI) actor := &activitystreams.Person{} remoteUser, err := getRemoteUser(app, actorIRI) if err != nil { if iErr, ok := err.(impart.HTTPError); ok { if iErr.Status == http.StatusNotFound { // Fetch remote actor log.Info("Not found; fetching actor %s remotely", actorIRI) actorResp, err := resolveIRI(app.cfg.App.Host, actorIRI) if err != nil { log.Error("Unable to get base actor! %v", err) return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actor."} } if err := unmarshalActor(actorResp, actor); err != nil { log.Error("Unable to unmarshal base actor! %v", err) return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actor."} } baseActor := &activitystreams.Person{} if err := unmarshalActor(actorResp, baseActor); err != nil { log.Error("Unable to unmarshal actual actor! %v", err) return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actual actor."} } // Fetch the actual actor using the owner field from the publicKey object actualActorResp, err := resolveIRI(app.cfg.App.Host, baseActor.PublicKey.Owner) if err != nil { log.Error("Unable to get actual actor! %v", err) return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actual actor."} } if err := unmarshalActor(actualActorResp, actor); err != nil { log.Error("Unable to unmarshal actual actor! %v", err) return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't parse actual actor."} } } else { return nil, nil, err } } else { return nil, nil, err } } else { actor = remoteUser.AsPerson() } return actor, remoteUser, nil } func GetProfileURLFromHandle(app *App, handle string) (string, error) { handle = strings.TrimLeft(handle, "@") actorIRI := "" parts := strings.Split(handle, "@") if len(parts) != 2 { return "", fmt.Errorf("invalid handle format") } domain := parts[1] // Check non-AP instances if siloProfileURL := silobridge.Profile(parts[0], domain); siloProfileURL != "" { return siloProfileURL, nil } remoteUser, err := getRemoteUserFromHandle(app, handle) if err != nil { // can't find using handle in the table but the table may already have this user without // handle from a previous version // TODO: Make this determination. We should know whether a user exists without a handle, or doesn't exist at all actorIRI = RemoteLookup(handle) _, errRemoteUser := getRemoteUser(app, actorIRI) // if it exists then we need to update the handle if errRemoteUser == nil { _, err := app.db.Exec("UPDATE remoteusers SET handle = ? WHERE actor_id = ?", handle, actorIRI) if err != nil { log.Error("Couldn't update handle '%s' for user %s", handle, actorIRI) } } else { // this probably means we don't have the user in the table so let's try to insert it // here we need to ask the server for the inboxes remoteActor, err := activityserve.NewRemoteActor(actorIRI) if err != nil { log.Error("Couldn't fetch remote actor: %v", err) } if debugging { log.Info("Got remote actor: %s %s %s %s %s", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), remoteActor.URL(), handle) } _, err = app.db.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox, url, handle) VALUES(?, ?, ?, ?, ?)", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), remoteActor.URL(), handle) if err != nil { log.Error("Couldn't insert remote user: %v", err) return "", err } actorIRI = remoteActor.URL() } } else if remoteUser.URL == "" { log.Info("Remote user %s URL empty, fetching", remoteUser.ActorID) newRemoteActor, err := activityserve.NewRemoteActor(remoteUser.ActorID) if err != nil { log.Error("Couldn't fetch remote actor: %v", err) } else { _, err := app.db.Exec("UPDATE remoteusers SET url = ? WHERE actor_id = ?", newRemoteActor.URL(), remoteUser.ActorID) if err != nil { log.Error("Couldn't update handle '%s' for user %s", handle, actorIRI) } else { actorIRI = newRemoteActor.URL() } } } else { actorIRI = remoteUser.URL } return actorIRI, nil } // unmarshal actor normalizes the actor response to conform to // the type Person from github.com/writeas/web-core/activitysteams // // some implementations return different context field types // this converts any non-slice contexts into a slice func unmarshalActor(actorResp []byte, actor *activitystreams.Person) error { // FIXME: Hubzilla has an object for the Actor's url: cannot unmarshal object into Go struct field Person.url of type string // flexActor overrides the Context field to allow // all valid representations during unmarshal flexActor := struct { activitystreams.Person Context json.RawMessage `json:"@context,omitempty"` }{} if err := json.Unmarshal(actorResp, &flexActor); err != nil { return err } actor.Endpoints = flexActor.Endpoints actor.Followers = flexActor.Followers actor.Following = flexActor.Following actor.ID = flexActor.ID actor.Icon = flexActor.Icon actor.Inbox = flexActor.Inbox actor.Name = flexActor.Name actor.Outbox = flexActor.Outbox actor.PreferredUsername = flexActor.PreferredUsername actor.PublicKey = flexActor.PublicKey actor.Summary = flexActor.Summary actor.Type = flexActor.Type actor.URL = flexActor.URL func(val interface{}) { switch val.(type) { case []interface{}: // already a slice, do nothing actor.Context = val.([]interface{}) default: actor.Context = []interface{}{val} } }(flexActor.Context) return nil } func parsePostIDFromURL(app *App, u *url.URL) (string, error) { // Get post ID from URL var collAlias, slug, postID string - if m := apCollectionPostIRIRegex.FindStringSubmatch(u.String()); len(m) == 3 { + path := u.Path + if subdir := app.cfg.App.SubdirectoryPath(); subdir != "" { + if path == subdir { + path = "/" + } else if strings.HasPrefix(path, subdir+"/") { + path = strings.TrimPrefix(path, subdir) + } + } + + if m := apCollectionPostIRIRegex.FindStringSubmatch(path); len(m) == 3 { collAlias = m[1] slug = m[2] - } else if m = apDraftPostIRIRegex.FindStringSubmatch(u.String()); len(m) == 2 { + } else if m = apDraftPostIRIRegex.FindStringSubmatch(path); len(m) == 2 { postID = m[1] } else { return "", fmt.Errorf("unable to match objectIRI: %s", u) } // Get postID if all we have is collection and slug if collAlias != "" && slug != "" { c, err := app.db.GetCollection(collAlias) if err != nil { return "", err } p, err := app.db.GetPost(slug, c.ID) if err != nil { return "", err } postID = p.ID } return postID, nil } func setCacheControl(w http.ResponseWriter, ttl time.Duration) { w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%.0f", ttl.Seconds())) } - -func logOutgoingActivity(label string, activity any) { - b, err := json.MarshalIndent(activity, "", " ") - if err != nil { - log.Error("Failed to marshal %s activity: %v", label, err) - return - } - log.Info("%s outgoing ActivityPub payload:\n%s", label, string(b)) -} diff --git a/activitypub_test.go b/activitypub_test.go index 7a1a89a..d267021 100644 --- a/activitypub_test.go +++ b/activitypub_test.go @@ -1,31 +1,113 @@ package writefreely import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" "testing" "github.com/writeas/web-core/activitystreams" ) var actorTestTable = []struct { Name string Resp []byte }{ { "Context as a string", []byte(`{"@context":"https://www.w3.org/ns/activitystreams"}`), }, { "Context as a list", []byte(`{"@context":["one string", "two strings"]}`), }, } func TestUnmarshalActor(t *testing.T) { for _, tc := range actorTestTable { actor := activitystreams.Person{} err := unmarshalActor(tc.Resp, &actor) if err != nil { t.Errorf("%s failed with error %s", tc.Name, err) } } } + +func TestParsePostIDFromURLDraftPathWithSubdirectory(t *testing.T) { + app := NewApp("testdata/config.ini") + if err := app.LoadConfig(); err != nil { + t.Fatalf("Could not create an app; %v", err) + } + app.cfg.App.Subdirectory = "/blog" + + u, err := url.Parse("https://example.com/blog/api/posts/abc123") + if err != nil { + t.Fatalf("could not parse URL: %v", err) + } + + postID, err := parsePostIDFromURL(app, u) + if err != nil { + t.Fatalf("expected parse to succeed, got error: %v", err) + } + if postID != "abc123" { + t.Fatalf("expected postID abc123, got %q", postID) + } +} + +func TestParsePostIDFromURLDraftPathAtRoot(t *testing.T) { + app := NewApp("testdata/config.ini") + if err := app.LoadConfig(); err != nil { + t.Fatalf("Could not create an app; %v", err) + } + app.cfg.App.Subdirectory = "" + + u, err := url.Parse("https://example.com/api/posts/xyz789") + if err != nil { + t.Fatalf("could not parse URL: %v", err) + } + + postID, err := parsePostIDFromURL(app, u) + if err != nil { + t.Fatalf("expected parse to succeed, got error: %v", err) + } + if postID != "xyz789" { + t.Fatalf("expected postID xyz789, got %q", postID) + } +} + +func TestHostMetaWebfingerTemplateUsesAbsoluteURL(t *testing.T) { + app := NewApp("testdata/config.ini") + if err := app.LoadConfig(); err != nil { + t.Fatalf("Could not create an app; %v", err) + } + app.cfg.App.Host = "https://example.com" + app.cfg.App.Subdirectory = "/blog" + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/blog/.well-known/host-meta", nil) + if err := handleViewHostMeta(app, rec, req); err != nil { + t.Fatalf("host-meta handler failed: %v", err) + } + + body := rec.Body.String() + expected := "https://example.com/blog/.well-known/webfinger?resource={uri}" + if !strings.Contains(body, expected) { + t.Fatalf("expected host-meta template URL %q in response body, got %q", expected, body) + } +} + +func TestNodeInfoConfigBaseURLUsesAbsoluteHost(t *testing.T) { + app := NewApp("testdata/config.ini") + if err := app.LoadConfig(); err != nil { + t.Fatalf("Could not create an app; %v", err) + } + app.cfg.App.Host = "https://example.com" + app.cfg.App.Subdirectory = "/blog" + app.cfg.App.SingleUser = false + + nc := nodeInfoConfig(nil, app.cfg) + if nc.BaseURL != "https://example.com/blog" { + t.Fatalf("expected base URL with subdirectory, got %q", nc.BaseURL) + } +} diff --git a/app.go b/app.go index fed36fb..6d71cfc 100644 --- a/app.go +++ b/app.go @@ -1,1017 +1,1023 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "crypto/tls" "database/sql" _ "embed" "fmt" "html/template" "net" "net/http" "net/url" "os" "os/signal" "path/filepath" "regexp" "strings" "syscall" "time" "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/gorilla/sessions" "github.com/manifoldco/promptui" stripmd "github.com/writeas/go-strip-markdown/v2" "github.com/writeas/impart" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/converter" "github.com/writeas/web-core/log" "golang.org/x/crypto/acme/autocert" "github.com/writefreely/writefreely/author" "github.com/writefreely/writefreely/config" "github.com/writefreely/writefreely/key" "github.com/writefreely/writefreely/migrations" "github.com/writefreely/writefreely/page" ) const ( staticDir = "static" assumedTitleLen = 80 postsPerPage = 10 postsPerArchPage = 40 serverSoftware = "WriteFreely" softwareURL = "https://writefreely.org" ) var ( debugging bool // Software version can be set from git env using -ldflags softwareVer = "0.16.0" // DEPRECATED VARS isSingleUser bool + + // Canonical URL helpers for code paths that still build URLs from shared globals. + canonicalAppHost string + canonicalSubdir string ) // App holds data and configuration for an individual WriteFreely instance. type App struct { router *mux.Router shttp *http.ServeMux db *datastore cfg *config.Config cfgFile string keys *key.Keychain sessionStore sessions.Store formDecoder *schema.Decoder updates *updatesCache timeline *localTimeline } // DB returns the App's datastore func (app *App) DB() *datastore { return app.db } // Router returns the App's router func (app *App) Router() *mux.Router { return app.router } // Config returns the App's current configuration. func (app *App) Config() *config.Config { return app.cfg } // SetConfig updates the App's Config to the given value. func (app *App) SetConfig(cfg *config.Config) { app.cfg = cfg } // SetKeys updates the App's Keychain to the given value. func (app *App) SetKeys(k *key.Keychain) { app.keys = k } func (app *App) SessionStore() sessions.Store { return app.sessionStore } func (app *App) SetSessionStore(s sessions.Store) { app.sessionStore = s } // Apper is the interface for getting data into and out of a WriteFreely // instance (or "App"). // // App returns the App for the current instance. // // LoadConfig reads an app configuration into the App, returning any error // encountered. // // SaveConfig persists the current App configuration. // // LoadKeys reads the App's encryption keys and loads them into its // key.Keychain. type Apper interface { App() *App LoadConfig() error SaveConfig(*config.Config) error LoadKeys() error ReqLog(r *http.Request, status int, timeSince time.Duration) string } // App returns the App func (app *App) App() *App { return app } // LoadConfig loads and parses a config file. func (app *App) LoadConfig() error { log.Info("Loading %s configuration...", app.cfgFile) cfg, err := config.Load(app.cfgFile) if err != nil { log.Error("Unable to load configuration: %v", err) os.Exit(1) return err } app.cfg = cfg return nil } // SaveConfig saves the given Config to disk -- namely, to the App's cfgFile. func (app *App) SaveConfig(c *config.Config) error { return config.Save(c, app.cfgFile) } // LoadKeys reads all needed keys from disk into the App. In order to use the // configured `Server.KeysParentDir`, you must call initKeyPaths(App) before // this. func (app *App) LoadKeys() error { var err error app.keys = &key.Keychain{} if debugging { log.Info(" %s", emailKeyPath) } executable, err := os.Executable() if err != nil { executable = "writefreely" } else { executable = filepath.Base(executable) } app.keys.EmailKey, err = os.ReadFile(emailKeyPath) if err != nil { return err } if debugging { log.Info(" %s", cookieAuthKeyPath) } app.keys.CookieAuthKey, err = os.ReadFile(cookieAuthKeyPath) if err != nil { return err } if debugging { log.Info(" %s", cookieKeyPath) } app.keys.CookieKey, err = os.ReadFile(cookieKeyPath) if err != nil { return err } if debugging { log.Info(" %s", csrfKeyPath) } app.keys.CSRFKey, err = os.ReadFile(csrfKeyPath) if err != nil { if os.IsNotExist(err) { log.Error(`Missing key: %s. Run this command to generate missing keys: %s keys generate `, csrfKeyPath, executable) } return err } return nil } func (app *App) ReqLog(r *http.Request, status int, timeSince time.Duration) string { return fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, timeSince, r.UserAgent()) } // handleViewHome shows page at root path. It checks the configuration and // authentication state to show the correct page. func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { if app.cfg.App.SingleUser { // Render blog index return handleViewCollection(app, w, r) } // Multi-user instance forceLanding := r.FormValue("landing") == "1" if !forceLanding { // Show correct page based on user auth status and configured landing path u := getUserSession(app, r) if app.cfg.App.Chorus { // This instance is focused on reading, so show Reader on home route if not // private or a private-instance user is logged in. if !app.cfg.App.Private || u != nil { return viewLocalTimeline(app, w, r) } } if u != nil { // User is logged in, so show the Pad return handleViewPad(app, w, r) } if app.cfg.App.Private { return viewLogin(app, w, r) } if land := app.cfg.App.LandingPath(); land != "/" { return impart.HTTPError{http.StatusFound, land} } } return handleViewLanding(app, w, r) } func handleViewLanding(app *App, w http.ResponseWriter, r *http.Request) error { forceLanding := r.FormValue("landing") == "1" p := struct { page.StaticPage *OAuthButtons Flashes []template.HTML Banner template.HTML Content template.HTML ForcedLanding bool }{ StaticPage: pageForReq(app, r), OAuthButtons: NewOAuthButtons(app.Config()), ForcedLanding: forceLanding, } banner, err := getLandingBanner(app) if err != nil { log.Error("unable to get landing banner: %v", err) return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get banner: %v", err)} } p.Banner = template.HTML(applyMarkdown([]byte(banner.Content), "", app.cfg)) content, err := getLandingBody(app) if err != nil { log.Error("unable to get landing content: %v", err) return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get content: %v", err)} } p.Content = template.HTML(applyMarkdown([]byte(content.Content), "", app.cfg)) // Get error messages session, err := app.sessionStore.Get(r, cookieName) if err != nil { // Ignore this log.Error("Unable to get session in handleViewHome; ignoring: %v", err) } flashes, _ := getSessionFlashes(app, w, r, session) for _, flash := range flashes { p.Flashes = append(p.Flashes, template.HTML(flash)) } // Show landing page return renderPage(w, "landing.tmpl", p) } func handleTemplatedPage(app *App, w http.ResponseWriter, r *http.Request, t *template.Template) error { p := struct { page.StaticPage ContentTitle string Content template.HTML PlainContent string Updated string AboutStats *InstanceStats }{ StaticPage: pageForReq(app, r), } - if r.URL.Path == "/about" || r.URL.Path == "/contact" || r.URL.Path == "/privacy" { + path := app.cfg.App.StripSubdirectory(r.URL.Path) + if path == "/about" || path == "/contact" || path == "/privacy" { var c *instanceContent var err error - if r.URL.Path == "/about" { + if path == "/about" { c, err = getAboutPage(app) // Fetch stats p.AboutStats = &InstanceStats{} p.AboutStats.NumPosts, _ = app.db.GetTotalPosts() p.AboutStats.NumBlogs, _ = app.db.GetTotalCollections() - } else if r.URL.Path == "/contact" { + } else if path == "/contact" { c, err = getContactPage(app) if c.Updated.IsZero() { // Page was never set up, so return 404 return ErrPostNotFound } } else { c, err = getPrivacyPage(app) } if err != nil { return err } p.ContentTitle = c.Title.String p.Content = template.HTML(applyMarkdown([]byte(c.Content), "", app.cfg)) p.PlainContent = shortPostDescription(stripmd.Strip(c.Content)) if !c.Updated.IsZero() { p.Updated = c.Updated.Format("January 2, 2006") } } // Serve templated page err := t.ExecuteTemplate(w, "base", p) if err != nil { log.Error("Unable to render page: %v", err) } return nil } func pageForReq(app *App, r *http.Request) page.StaticPage { p := page.StaticPage{ AppCfg: app.cfg.App, - Path: r.URL.Path, + Path: app.cfg.App.StripSubdirectory(r.URL.Path), Version: "v" + softwareVer, } // Use custom style, if file exists if _, err := os.Stat(filepath.Join(app.cfg.Server.StaticParentDir, staticDir, "local", "custom.css")); err == nil { p.CustomCSS = true } // Add user information, if given var u *User accessToken := r.FormValue("t") if accessToken != "" { userID := app.db.GetUserID(accessToken) if userID != -1 { var err error u, err = app.db.GetUserByID(userID) if err == nil { p.Username = u.Username } } } else { u = getUserSession(app, r) if u != nil { p.Username = u.Username p.IsAdmin = u != nil && u.IsAdmin() p.CanInvite = canUserInvite(app.cfg, p.IsAdmin) } } p.CanViewReader = !app.cfg.App.Private || u != nil return p } var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$") // Initialize loads the app configuration and initializes templates, keys, // session, route handlers, and the database connection. func Initialize(apper Apper, debug bool) (*App, error) { debugging = debug apper.LoadConfig() // Load templates err := InitTemplates(apper.App().Config()) if err != nil { return nil, fmt.Errorf("load templates: %s", err) } // Load keys and set up session initKeyPaths(apper.App()) // TODO: find a better way to do this, since it's unneeded in all Apper implementations err = InitKeys(apper) if err != nil { return nil, fmt.Errorf("init keys: %s", err) } apper.App().InitUpdates() apper.App().InitSession() apper.App().InitDecoder() err = ConnectToDatabase(apper.App()) if err != nil { return nil, fmt.Errorf("connect to DB: %s", err) } initActivityPub(apper.App()) if apper.App().cfg.Email.Enabled() { log.Info("Starting publish jobs queue...") go startPublishJobsQueue(apper.App()) } else { log.Info("[jobs] Not starting publish jobs queue: no email provider is configured.") } // Handle local timeline, if enabled if apper.App().cfg.App.LocalTimeline { log.Info("Initializing local timeline...") initLocalTimeline(apper.App()) } return apper.App(), nil } func Serve(app *App, r *mux.Router) { log.Info("Going to serve...") isSingleUser = app.cfg.App.SingleUser + canonicalAppHost = strings.TrimSuffix(app.cfg.App.Host, "/") + canonicalSubdir = app.cfg.App.SubdirectoryPath() app.cfg.Server.Dev = debugging // Handle shutdown c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c log.Info("Shutting down...") shutdown(app) log.Info("Done.") os.Exit(0) }() // Start gopher server if app.cfg.Server.GopherPort > 0 && !app.cfg.App.Private { go initGopher(app) } // Start web application server var bindAddress = app.cfg.Server.Bind if bindAddress == "" { bindAddress = "localhost" } var err error if app.cfg.IsSecureStandalone() { if app.cfg.Server.Autocert { m := &autocert.Manager{ Prompt: autocert.AcceptTOS, Cache: autocert.DirCache(app.cfg.Server.TLSCertPath), } host, err := url.Parse(app.cfg.App.Host) if err != nil { log.Error("[WARNING] Unable to parse configured host! %s", err) log.Error(`[WARNING] ALL hosts are allowed, which can open you to an attack where clients connect to a server by IP address and pretend to be asking for an incorrect host name, and cause you to reach the CA's rate limit for certificate requests. We recommend supplying a valid host name.`) log.Info("Using autocert on ANY host") } else { log.Info("Using autocert on host %s", host.Host) m.HostPolicy = autocert.HostWhitelist(host.Host) } s := &http.Server{ Addr: ":https", Handler: r, TLSConfig: &tls.Config{ GetCertificate: m.GetCertificate, }, } s.SetKeepAlivesEnabled(false) go func() { log.Info("Serving redirects on http://%s:80", bindAddress) err = http.ListenAndServe(":80", m.HTTPHandler(nil)) log.Error("Unable to start redirect server: %v", err) }() log.Info("Serving on https://%s:443", bindAddress) log.Info("---") err = s.ListenAndServeTLS("", "") } else { go func() { log.Info("Serving redirects on http://%s:80", bindAddress) err = http.ListenAndServe(fmt.Sprintf("%s:80", bindAddress), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, app.cfg.App.Host, http.StatusMovedPermanently) + loc := app.cfg.App.AbsoluteURL(r.URL.Path) + if r.URL.RawQuery != "" { + loc += "?" + r.URL.RawQuery + } + http.Redirect(w, r, loc, http.StatusMovedPermanently) })) log.Error("Unable to start redirect server: %v", err) }() log.Info("Serving on https://%s:443", bindAddress) log.Info("Using manual certificates") log.Info("---") err = http.ListenAndServeTLS(fmt.Sprintf("%s:443", bindAddress), app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, r) } } else { network := "tcp" protocol := "http" if strings.HasPrefix(bindAddress, "/") { network = "unix" protocol = "http+unix" // old sockets will remain after server closes; // we need to delete them in order to open new ones err = os.Remove(bindAddress) if err != nil && !os.IsNotExist(err) { log.Error("%s already exists but could not be removed: %v", bindAddress, err) os.Exit(1) } } else { bindAddress = fmt.Sprintf("%s:%d", bindAddress, app.cfg.Server.Port) } log.Info("Serving on %s://%s", protocol, bindAddress) log.Info("---") listener, err := net.Listen(network, bindAddress) if err != nil { log.Error("Could not bind to address: %v", err) os.Exit(1) } if network == "unix" { err = os.Chmod(bindAddress, 0o666) if err != nil { log.Error("Could not update socket permissions: %v", err) os.Exit(1) } } defer listener.Close() err = http.Serve(listener, r) } if err != nil { log.Error("Unable to start: %v", err) os.Exit(1) } } func (app *App) InitDecoder() { // TODO: do this at the package level, instead of the App level // Initialize modules app.formDecoder = schema.NewDecoder() app.formDecoder.RegisterConverter(converter.NullJSONString{}, converter.ConvertJSONNullString) app.formDecoder.RegisterConverter(converter.NullJSONBool{}, converter.ConvertJSONNullBool) app.formDecoder.RegisterConverter(sql.NullString{}, converter.ConvertSQLNullString) app.formDecoder.RegisterConverter(sql.NullBool{}, converter.ConvertSQLNullBool) app.formDecoder.RegisterConverter(sql.NullInt64{}, converter.ConvertSQLNullInt64) app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64) } // ConnectToDatabase validates and connects to the configured database, then // tests the connection. func ConnectToDatabase(app *App) error { // Check database configuration if app.cfg.Database.Type == driverMySQL && app.cfg.Database.User == "" { return fmt.Errorf("Database user not set.") } if app.cfg.Database.Host == "" { app.cfg.Database.Host = "localhost" } if app.cfg.Database.Database == "" { app.cfg.Database.Database = "writefreely" } // TODO: check err connectToDatabase(app) // Test database connection err := app.db.Ping() if err != nil { return fmt.Errorf("Database ping failed: %s", err) } log.Info("Connected to database.") ver, err := app.db.version() if err != nil { log.Error("Unable to get DB version: %v", err) } else { log.Info("Database version: %v", ver) if app.cfg.Database.Type == driverMySQL && strings.HasPrefix(ver, "5.") { log.Info("Enabling compatibility for MySQL v5.x") app.db.useSpencerRegex = true } } return nil } // FormatVersion constructs the version string for the application func FormatVersion() string { return serverSoftware + " " + softwareVer } // OutputVersion prints out the version of the application. func OutputVersion() { fmt.Println(FormatVersion()) } // NewApp creates a new app instance. func NewApp(cfgFile string) *App { return &App{ cfgFile: cfgFile, } } // CreateConfig creates a default configuration and saves it to the app's cfgFile. func CreateConfig(app *App) error { log.Info("Creating configuration...") c := config.New() log.Info("Saving configuration %s...", app.cfgFile) err := config.Save(c, app.cfgFile) if err != nil { return fmt.Errorf("Unable to save configuration: %v", err) } return nil } // DoConfig runs the interactive configuration process. func DoConfig(app *App, configSections string) { if configSections == "" { configSections = "server db app" } // let's check there aren't any garbage in the list configSectionsArray := strings.Split(configSections, " ") for _, element := range configSectionsArray { if element != "server" && element != "db" && element != "app" { log.Error("Invalid argument to --sections. Valid arguments are only \"server\", \"db\" and \"app\"") os.Exit(1) } } d, err := config.Configure(app.cfgFile, configSections) if err != nil { log.Error("Unable to configure: %v", err) os.Exit(1) } app.cfg = d.Config connectToDatabase(app) defer shutdown(app) if !app.db.DatabaseInitialized() { err = adminInitDatabase(app) if err != nil { log.Error(err.Error()) os.Exit(1) } } else { log.Info("Database already initialized.") } if d.User != nil { u := &User{ Username: d.User.Username, HashedPass: d.User.HashedPass, Created: time.Now().Truncate(time.Second).UTC(), } // Create blog log.Info("Creating user %s...\n", u.Username) err = app.db.CreateUser(app.cfg, u, app.cfg.App.SiteName, "") if err != nil { log.Error("Unable to create user: %s", err) os.Exit(1) } log.Info("Done!") } os.Exit(0) } // GenerateKeyFiles creates app encryption keys and saves them into the configured KeysParentDir. func GenerateKeyFiles(app *App) error { // Read keys path from config app.LoadConfig() // Create keys dir if it doesn't exist yet fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir) if _, err := os.Stat(fullKeysDir); os.IsNotExist(err) { err = os.Mkdir(fullKeysDir, 0700) if err != nil { return err } } // Generate keys initKeyPaths(app) // TODO: use something like https://github.com/hashicorp/go-multierror to return errors var keyErrs error err := generateKey(emailKeyPath) if err != nil { keyErrs = err } err = generateKey(cookieAuthKeyPath) if err != nil { keyErrs = err } err = generateKey(cookieKeyPath) if err != nil { keyErrs = err } err = generateKey(csrfKeyPath) if err != nil { keyErrs = err } return keyErrs } // CreateSchema creates all database tables needed for the application. func CreateSchema(apper Apper) error { apper.LoadConfig() connectToDatabase(apper.App()) defer shutdown(apper.App()) err := adminInitDatabase(apper.App()) if err != nil { return err } return nil } // Migrate runs all necessary database migrations. func Migrate(apper Apper) error { apper.LoadConfig() connectToDatabase(apper.App()) defer shutdown(apper.App()) err := migrations.Migrate(migrations.NewDatastore(apper.App().db.DB, apper.App().db.driverName)) if err != nil { return fmt.Errorf("migrate: %s", err) } return nil } // ResetPassword runs the interactive password reset process. func ResetPassword(apper Apper, username string) error { // Connect to the database apper.LoadConfig() connectToDatabase(apper.App()) defer shutdown(apper.App()) // Fetch user u, err := apper.App().db.GetUserForAuth(username) if err != nil { log.Error("Get user: %s", err) os.Exit(1) } // Prompt for new password prompt := promptui.Prompt{ Templates: &promptui.PromptTemplates{ Success: "{{ . | bold | faint }}: ", }, Label: "New password", Mask: '*', } newPass, err := prompt.Run() if err != nil { log.Error("%s", err) os.Exit(1) } // Do the update log.Info("Updating...") err = adminResetPassword(apper.App(), u, newPass) if err != nil { log.Error("%s", err) os.Exit(1) } log.Info("Success.") return nil } // DoDeleteAccount runs the confirmation and account delete process. func DoDeleteAccount(apper Apper, username string) error { // Connect to the database apper.LoadConfig() connectToDatabase(apper.App()) defer shutdown(apper.App()) // check user exists u, err := apper.App().db.GetUserForAuth(username) if err != nil { log.Error("%s", err) os.Exit(1) } userID := u.ID // do not delete the admin account // TODO: check for other admins and skip? if u.IsAdmin() { log.Error("Can not delete admin account") os.Exit(1) } // confirm deletion, w/ w/out posts prompt := promptui.Prompt{ Templates: &promptui.PromptTemplates{ Success: "{{ . | bold | faint }}: ", }, Label: fmt.Sprintf("Really delete user : %s", username), IsConfirm: true, } _, err = prompt.Run() if err != nil { log.Info("Aborted...") os.Exit(0) } log.Info("Deleting...") err = apper.App().db.DeleteAccount(userID) if err != nil { log.Error("%s", err) os.Exit(1) } log.Info("Success.") return nil } func connectToDatabase(app *App) { log.Info("Connecting to %s database...", app.cfg.Database.Type) var db *sql.DB var err error if app.cfg.Database.Type == driverMySQL { db, err = sql.Open(app.cfg.Database.Type, fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s&tls=%t", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database, url.QueryEscape(time.Local.String()), app.cfg.Database.TLS)) db.SetMaxOpenConns(50) } else if app.cfg.Database.Type == driverSQLite { if !SQLiteEnabled { log.Error("Invalid database type '%s'. Binary wasn't compiled with SQLite3 support.", app.cfg.Database.Type) os.Exit(1) } if app.cfg.Database.FileName == "" { log.Error("SQLite database filename value in config.ini is empty.") os.Exit(1) } db, err = sql.Open("sqlite3_with_regex", app.cfg.Database.FileName+"?parseTime=true&cached=shared") db.SetMaxOpenConns(2) } else { log.Error("Invalid database type '%s'. Only 'mysql' and 'sqlite3' are supported right now.", app.cfg.Database.Type) os.Exit(1) } if err != nil { log.Error("%s", err) os.Exit(1) } app.db = &datastore{DB: db, driverName: app.cfg.Database.Type} } func shutdown(app *App) { log.Info("Closing database connection...") app.db.Close() if strings.HasPrefix(app.cfg.Server.Bind, "/") { // Clean up socket log.Info("Removing socket file...") err := os.Remove(app.cfg.Server.Bind) if err != nil { - if os.IsNotExist(err) { - // Safely ignore, in cases like initializing / migrating DB (see #790) - log.Info("No socket file; ignoring...") - } else { - log.Error("Unable to remove socket: %s", err) - os.Exit(1) - } + log.Error("Unable to remove socket: %s", err) + os.Exit(1) } log.Info("Success.") } } // CreateUser creates a new admin or normal user from the given credentials. func CreateUser(apper Apper, username, password string, isAdmin bool) error { // Create an admin user with --create-admin apper.LoadConfig() connectToDatabase(apper.App()) defer shutdown(apper.App()) // Ensure an admin / first user doesn't already exist firstUser, _ := apper.App().db.GetUserByID(1) if isAdmin { // Abort if trying to create admin user, but one already exists if firstUser != nil { return fmt.Errorf("Admin user already exists (%s). Create a regular user with: writefreely user create [USER]:[PASSWORD]", firstUser.Username) } } else { // Abort if trying to create regular user, but no admin exists yet if firstUser == nil { return fmt.Errorf("No admin user exists yet. Create an admin first with: writefreely user create --admin [USER]:[PASSWORD]") } } // Create the user // Normalize and validate username desiredUsername := username username = getSlug(username, "") usernameDesc := username if username != desiredUsername { usernameDesc += " (originally: " + desiredUsername + ")" } if !author.IsValidUsername(apper.App().cfg, username) { return fmt.Errorf("Username %s is invalid, reserved, or shorter than configured minimum length (%d characters).", usernameDesc, apper.App().cfg.App.MinUsernameLen) } // Hash the password hashedPass, err := auth.HashPass([]byte(password)) if err != nil { return fmt.Errorf("Unable to hash password: %v", err) } u := &User{ Username: username, HashedPass: hashedPass, Created: time.Now().Truncate(time.Second).UTC(), } userType := "user" if isAdmin { userType = "admin" } log.Info("Creating %s %s...", userType, usernameDesc) err = apper.App().db.CreateUser(apper.App().Config(), u, desiredUsername, "") if err != nil { return fmt.Errorf("Unable to create user: %s", err) } log.Info("Done!") return nil } //go:embed schema.sql var schemaSql string //go:embed sqlite.sql var sqliteSql string func adminInitDatabase(app *App) error { var schema string if app.cfg.Database.Type == driverSQLite { schema = sqliteSql } else { schema = schemaSql } tblReg := regexp.MustCompile("CREATE TABLE (IF NOT EXISTS )?`([a-z_]+)`") queries := strings.Split(string(schema), ";\n") for _, q := range queries { if strings.TrimSpace(q) == "" { continue } parts := tblReg.FindStringSubmatch(q) if len(parts) >= 3 { log.Info("Creating table %s...", parts[2]) } else { log.Info("Creating table ??? (Weird query) No match in: %v", parts) } _, err := app.db.Exec(q) if err != nil { log.Error("%s", err) } else { log.Info("Created.") } } // Set up migrations table log.Info("Initializing appmigrations table...") err := migrations.SetInitialMigrations(migrations.NewDatastore(app.db.DB, app.db.driverName)) if err != nil { return fmt.Errorf("Unable to set initial migrations: %v", err) } log.Info("Running migrations...") err = migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName)) if err != nil { return fmt.Errorf("migrate: %s", err) } log.Info("Done.") return nil } // ServerUserAgent returns a User-Agent string to use in external requests. The // hostName parameter may be left empty. func ServerUserAgent(hostName string) string { hostUAStr := "" if hostName != "" { hostUAStr = "; +" + hostName } return "Go (" + serverSoftware + "/" + softwareVer + hostUAStr + ")" } diff --git a/author/author.go b/author/author.go index 0c2ea57..a95eb61 100644 --- a/author/author.go +++ b/author/author.go @@ -1,137 +1,136 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package author import ( + "github.com/writeas/web-core/log" + "github.com/writefreely/writefreely/config" "os" "path/filepath" "regexp" - - "github.com/writeas/web-core/log" - "github.com/writefreely/writefreely/config" ) // Regex pattern for valid usernames var validUsernameReg = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-]*$") // List of reserved usernames var reservedUsernames = map[string]bool{ "a": true, "about": true, "add": true, "admin": true, "administrator": true, "adminzone": true, "api": true, "article": true, "articles": true, "auth": true, "authenticate": true, "browse": true, "c": true, "categories": true, "category": true, "changes": true, "community": true, "create": true, "css": true, "data": true, "dev": true, "developers": true, "draft": true, "drafts": true, "edit": true, "edits": true, "faq": true, "feed": true, "feedback": true, "guide": true, "guides": true, "help": true, "index": true, "invite": true, "js": true, "login": true, "logout": true, "me": true, "media": true, "meta": true, "metadata": true, "new": true, "news": true, "oauth": true, "post": true, "posts": true, "privacy": true, "publication": true, "publications": true, "publish": true, "random": true, "read": true, "reader": true, "register": true, "remove": true, "signin": true, "signout": true, "signup": true, "start": true, "status": true, "summary": true, "support": true, "tag": true, "tags": true, "team": true, "template": true, "templates": true, "terms": true, "terms-of-service": true, "termsofservice": true, "theme": true, "themes": true, "tips": true, "tos": true, "update": true, "updates": true, "user": true, "users": true, "yourname": true, } // IsValidUsername returns true if a given username is neither reserved nor // of the correct format. func IsValidUsername(cfg *config.Config, username string) bool { // Username has to be above a character limit if len(username) < cfg.App.MinUsernameLen { return false } // Username is invalid if page with the same name exists. So traverse // available pages, adding them to reservedUsernames map that'll be checked // later. err := filepath.Walk(filepath.Join(cfg.Server.PagesParentDir, "pages"), func(path string, i os.FileInfo, err error) error { if err != nil { return err } reservedUsernames[i.Name()] = true return nil }) if err != nil { log.Error("[IMPORTANT WARNING]: Could not determine IsValidUsername! %s", err) return false } // Username is invalid if it is reserved! if _, reserved := reservedUsernames[username]; reserved { return false } // TODO: use correct regexp function here return len(validUsernameReg.FindStringSubmatch(username)) > 0 } diff --git a/cmd/writefreely/main.go b/cmd/writefreely/main.go index cd7ed69..4a1fbcf 100644 --- a/cmd/writefreely/main.go +++ b/cmd/writefreely/main.go @@ -1,183 +1,185 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package main import ( "fmt" "os" "strings" "github.com/gorilla/mux" "github.com/urfave/cli/v2" "github.com/writeas/web-core/log" "github.com/writefreely/writefreely" ) func main() { cli.VersionPrinter = func(c *cli.Context) { fmt.Printf("%s\n", c.App.Version) } app := &cli.App{ Name: "WriteFreely", Usage: "A beautifully pared-down blogging platform", Version: writefreely.FormatVersion(), Action: legacyActions, // legacy due to use of flags for switching actions Flags: []cli.Flag{ &cli.BoolFlag{ Name: "create-config", Value: false, Usage: "Generate a basic configuration", Hidden: true, }, &cli.BoolFlag{ Name: "config", Value: false, Usage: "Interactive configuration process", Hidden: true, }, &cli.StringFlag{ Name: "sections", Value: "server db app", Usage: "Which sections of the configuration to go through (requires --config)\n" + "valid values are any combination of 'server', 'db' and 'app' \n" + "example: writefreely --config --sections \"db app\"", Hidden: true, }, &cli.BoolFlag{ Name: "gen-keys", Value: false, Usage: "Generate encryption and authentication keys", Hidden: true, }, &cli.BoolFlag{ Name: "init-db", Value: false, Usage: "Initialize app database", Hidden: true, }, &cli.BoolFlag{ Name: "migrate", Value: false, Usage: "Migrate the database", Hidden: true, }, &cli.StringFlag{ Name: "create-admin", Usage: "Create an admin with the given username:password", Hidden: true, }, &cli.StringFlag{ Name: "create-user", Usage: "Create a regular user with the given username:password", Hidden: true, }, &cli.StringFlag{ Name: "delete-user", Usage: "Delete a user with the given username", Hidden: true, }, &cli.StringFlag{ Name: "reset-pass", Usage: "Reset the given user's password", Hidden: true, }, }, // legacy flags (set to hidden to eventually switch to bash-complete compatible format) } defaultFlags := []cli.Flag{ &cli.StringFlag{ Name: "c", Value: "config.ini", Usage: "Load configuration from `FILE`", }, &cli.BoolFlag{ Name: "debug", Value: false, Usage: "Enables debug logging", }, } app.Flags = append(app.Flags, defaultFlags...) app.Commands = []*cli.Command{ &cmdUser, &cmdDB, &cmdConfig, &cmdKeys, &cmdServe, } err := app.Run(os.Args) if err != nil { log.Error(err.Error()) os.Exit(1) } } func legacyActions(c *cli.Context) error { app := writefreely.NewApp(c.String("c")) switch true { case c.IsSet("create-config"): return writefreely.CreateConfig(app) case c.IsSet("config"): writefreely.DoConfig(app, c.String("sections")) return nil case c.IsSet("gen-keys"): return writefreely.GenerateKeyFiles(app) case c.IsSet("init-db"): return writefreely.CreateSchema(app) case c.IsSet("migrate"): return writefreely.Migrate(app) case c.IsSet("create-admin"): username, password, err := parseCredentials(c.String("create-admin")) if err != nil { return err } return writefreely.CreateUser(app, username, password, true) case c.IsSet("create-user"): username, password, err := parseCredentials(c.String("create-user")) if err != nil { return err } return writefreely.CreateUser(app, username, password, false) case c.IsSet("delete-user"): return writefreely.DoDeleteAccount(app, c.String("delete-user")) case c.IsSet("reset-pass"): return writefreely.ResetPassword(app, c.String("reset-pass")) } // Initialize the application var err error log.Info("Starting %s...", writefreely.FormatVersion()) app, err = writefreely.Initialize(app, c.Bool("debug")) if err != nil { return err } // Set app routes r := mux.NewRouter() - writefreely.InitRoutes(app, r) - app.InitStaticRoutes(r) + appRouter := mux.NewRouter() + app.InitStaticRoutes(appRouter) + writefreely.InitRoutes(app, appRouter) + writefreely.MountSubdirectory(app, r, appRouter) // Serve the application writefreely.Serve(app, r) return nil } func parseCredentials(credentialString string) (string, string, error) { creds := strings.Split(credentialString, ":") if len(creds) != 2 { return "", "", fmt.Errorf("invalid format for passed credentials, must be username:password") } return creds[0], creds[1], nil } diff --git a/cmd/writefreely/web.go b/cmd/writefreely/web.go index b91910c..cf229d2 100644 --- a/cmd/writefreely/web.go +++ b/cmd/writefreely/web.go @@ -1,48 +1,50 @@ /* * Copyright © 2020-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package main import ( "github.com/gorilla/mux" "github.com/urfave/cli/v2" "github.com/writeas/web-core/log" "github.com/writefreely/writefreely" ) var ( cmdServe cli.Command = cli.Command{ Name: "serve", Aliases: []string{"web"}, Usage: "Run web application", Action: serveAction, } ) func serveAction(c *cli.Context) error { // Initialize the application app := writefreely.NewApp(c.String("c")) var err error log.Info("Starting %s...", writefreely.FormatVersion()) app, err = writefreely.Initialize(app, c.Bool("debug")) if err != nil { return err } // Set app routes r := mux.NewRouter() - writefreely.InitRoutes(app, r) - app.InitStaticRoutes(r) + appRouter := mux.NewRouter() + app.InitStaticRoutes(appRouter) + writefreely.InitRoutes(app, appRouter) + writefreely.MountSubdirectory(app, r, appRouter) // Serve the application writefreely.Serve(app, r) return nil } diff --git a/collections.go b/collections.go index 95503dd..bbaf0a3 100644 --- a/collections.go +++ b/collections.go @@ -1,1462 +1,1481 @@ /* * Copyright © 2018-2022 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "database/sql" "encoding/json" "fmt" "html/template" "math" "net/http" "net/url" "regexp" "strconv" "strings" "unicode" "github.com/gorilla/mux" stripmd "github.com/writeas/go-strip-markdown/v2" "github.com/writeas/impart" "github.com/writeas/web-core/activitystreams" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/bots" "github.com/writeas/web-core/i18n" "github.com/writeas/web-core/log" "github.com/writeas/web-core/posts" "github.com/writefreely/writefreely/author" "github.com/writefreely/writefreely/config" "github.com/writefreely/writefreely/page" "github.com/writefreely/writefreely/spam" "golang.org/x/net/idna" ) const ( collAttrLetterReplyTo = "letter_reply_to" collMaxLengthTitle = 255 collMaxLengthDescription = 160 ) type ( // TODO: add Direction to db // TODO: add Language to db Collection struct { ID int64 `datastore:"id" json:"-"` Alias string `datastore:"alias" schema:"alias" json:"alias"` Title string `datastore:"title" schema:"title" json:"title"` Description string `datastore:"description" schema:"description" json:"description"` Direction string `schema:"dir" json:"dir,omitempty"` Language string `schema:"lang" json:"lang,omitempty"` StyleSheet string `datastore:"style_sheet" schema:"style_sheet" json:"style_sheet"` Script string `datastore:"script" schema:"script" json:"script,omitempty"` Signature string `datastore:"post_signature" schema:"signature" json:"-"` Public bool `datastore:"public" json:"public"` Visibility collVisibility `datastore:"private" json:"-"` Format string `datastore:"format" json:"format,omitempty"` Views int64 `json:"views"` OwnerID int64 `datastore:"owner_id" json:"-"` PublicOwner bool `datastore:"public_owner" json:"-"` URL string `json:"url,omitempty"` Monetization string `json:"monetization_pointer,omitempty"` Verification string `json:"verification_link"` db *datastore hostName string } CollectionObj struct { Collection TotalPosts int `json:"total_posts"` Owner *User `json:"owner,omitempty"` Posts *[]PublicPost `json:"posts,omitempty"` Format *CollectionFormat } DisplayCollection struct { *CollectionObj Prefix string NavSuffix string IsTopLevel bool CurrentPage int TotalPages int Silenced bool } CollectionNav struct { *Collection Path string SingleUser bool CanPost bool } SubmittedCollection struct { // Data used for updating a given collection ID int64 OwnerID uint64 // Form helpers PreferURL string `schema:"prefer_url" json:"prefer_url"` Privacy int `schema:"privacy" json:"privacy"` Pass string `schema:"password" json:"password"` MathJax bool `schema:"mathjax" json:"mathjax"` EmailSubs bool `schema:"email_subs" json:"email_subs"` Handle string `schema:"handle" json:"handle"` // Actual collection values updated in the DB Alias *string `schema:"alias" json:"alias"` Title *string `schema:"title" json:"title"` Description *string `schema:"description" json:"description"` StyleSheet *string `schema:"style_sheet" json:"style_sheet"` Script *string `schema:"script" json:"script"` Signature *string `schema:"signature" json:"signature"` Monetization *string `schema:"monetization_pointer" json:"monetization_pointer"` Verification *string `schema:"verification_link" json:"verification_link"` LetterReply *string `schema:"letter_reply" json:"letter_reply"` Visibility *int `schema:"visibility" json:"public"` Format *sql.NullString `schema:"format" json:"format"` } CollectionFormat struct { Format string } collectionReq struct { // Information about the collection request itself prefix, alias, domain string isCustomDomain bool // User-related fields isCollOwner bool isAuthorized bool } ) func (sc *SubmittedCollection) FediverseHandle() string { if sc.Handle == "" { return apCustomHandleDefault } return getSlug(sc.Handle, "") } // collVisibility represents the visibility level for the collection. type collVisibility int // Visibility levels. Values are bitmasks, stored in the database as // decimal numbers. If adding types, append them to this list. If removing, // replace the desired visibility with a new value. const CollUnlisted collVisibility = 0 const ( CollPublic collVisibility = 1 << iota CollPrivate CollProtected ) var collVisibilityStrings = map[string]collVisibility{ "unlisted": CollUnlisted, "public": CollPublic, "private": CollPrivate, "protected": CollProtected, } func defaultVisibility(cfg *config.Config) collVisibility { vis, ok := collVisibilityStrings[cfg.App.DefaultVisibility] if !ok { vis = CollUnlisted } return vis } func (cf *CollectionFormat) Ascending() bool { return cf.Format == "novel" } func (cf *CollectionFormat) ShowDates() bool { return cf.Format == "blog" } func (cf *CollectionFormat) PostsPerPage() int { if cf.Format == "novel" { return postsPerPage } return postsPerPage } // Valid returns whether or not a format value is valid. func (cf *CollectionFormat) Valid() bool { return cf.Format == "blog" || cf.Format == "novel" || cf.Format == "notebook" } // NewFormat creates a new CollectionFormat object from the Collection. func (c *Collection) NewFormat() *CollectionFormat { cf := &CollectionFormat{Format: c.Format} // Fill in default format if cf.Format == "" { cf.Format = "blog" } return cf } func (c *Collection) IsInstanceColl() bool { ur, _ := url.Parse(c.hostName) return c.Alias == ur.Host } func (c *Collection) IsUnlisted() bool { return c.Visibility == 0 } func (c *Collection) IsPrivate() bool { return c.Visibility&CollPrivate != 0 } func (c *Collection) IsProtected() bool { return c.Visibility&CollProtected != 0 } func (c *Collection) IsPublic() bool { return c.Visibility&CollPublic != 0 } func (c *Collection) FriendlyVisibility() string { if c.IsPrivate() { return "Private" } if c.IsPublic() { return "Public" } if c.IsProtected() { return "Password-protected" } return "Unlisted" } func (c *Collection) ShowFooterBranding() bool { // TODO: implement this setting return true } // CanonicalURL returns a fully-qualified URL to the collection. func (c *Collection) CanonicalURL() string { return c.RedirectingCanonicalURL(false) } func (c *Collection) DisplayCanonicalURL() string { us := c.CanonicalURL() u, err := url.Parse(us) if err != nil { return us } p := u.Path if p == "/" { p = "" } d := u.Hostname() d, _ = idna.ToUnicode(d) return d + p } // RedirectingCanonicalURL returns the fully-qualified canonical URL for the Collection, with a trailing slash. The // hostName field needs to be populated for this to work correctly. func (c *Collection) RedirectingCanonicalURL(isRedir bool) string { if c.hostName == "" { // If this is true, the human programmers screwed up. So ask for a bug report and fail, fail, fail log.Error("[PROGRAMMER ERROR] WARNING: Collection.hostName is empty! Federation and many other things will fail! If you're seeing this in the wild, please report this bug and let us know what you were doing just before this: https://github.com/writefreely/writefreely/issues/new?template=bug_report.md") } + hostName := strings.TrimSuffix(c.hostName, "/") + if canonicalSubdir != "" && canonicalAppHost != "" { + hostU, hostErr := url.Parse(hostName) + appU, appErr := url.Parse(canonicalAppHost) + if hostErr == nil && appErr == nil && strings.EqualFold(hostU.Host, appU.Host) { + if hostU.Path == "" || hostU.Path == "/" { + hostU.Path = canonicalSubdir + hostName = strings.TrimSuffix(hostU.String(), "/") + } + } + } if isSingleUser { - return c.hostName + "/" + return hostName + "/" } - return fmt.Sprintf("%s/%s/", c.hostName, c.Alias) + return fmt.Sprintf("%s/%s/", hostName, c.Alias) } // PrevPageURL provides a full URL for the previous page of collection posts, // returning a /page/N result for pages >1 func (c *Collection) PrevPageURL(prefix, navSuffix string, n int, tl bool) string { u := "" if n == 2 { // Previous page is 1; no need for /page/ prefix if prefix == "" { u = navSuffix + "/" } // Else leave off trailing slash } else { u = fmt.Sprintf("%s/page/%d", navSuffix, n-1) } if tl { return u } return "/" + prefix + c.Alias + u } // NextPageURL provides a full URL for the next page of collection posts func (c *Collection) NextPageURL(prefix, navSuffix string, n int, tl bool) string { if tl { return fmt.Sprintf("%s/page/%d", navSuffix, n+1) } return fmt.Sprintf("/%s%s%s/page/%d", prefix, c.Alias, navSuffix, n+1) } func (c *Collection) DisplayTitle() string { if c.Title != "" { return c.Title } return c.Alias } func (c *Collection) StyleSheetDisplay() template.CSS { return template.CSS(c.StyleSheet) } // ForPublic modifies the Collection for public consumption, such as via // the API. func (c *Collection) ForPublic() { c.URL = c.CanonicalURL() } var isAvatarChar = regexp.MustCompile("[a-z0-9]").MatchString func (c *Collection) PersonObject(ids ...int64) *activitystreams.Person { accountRoot := c.FederatedAccount() p := activitystreams.NewPerson(accountRoot) p.URL = c.CanonicalURL() uname := c.Alias p.PreferredUsername = uname p.Name = c.DisplayTitle() p.Summary = c.Description if p.Name != "" { if av := c.AvatarURL(); av != "" { p.Icon = activitystreams.Image{ Type: "Image", MediaType: "image/png", URL: av, } } } collID := c.ID if len(ids) > 0 { collID = ids[0] } pub, priv := c.db.GetAPActorKeys(collID) if pub != nil { p.AddPubKey(pub) p.SetPrivKey(priv) } return p } func (c *Collection) AvatarURL() string { fl := string(unicode.ToLower([]rune(c.DisplayTitle())[0])) if !isAvatarChar(fl) { return "" } return c.hostName + "/img/avatars/" + fl + ".png" } func (c *Collection) FederatedAPIBase() string { return c.hostName + "/" } func (c *Collection) FederatedAccount() string { accountUser := c.Alias return c.FederatedAPIBase() + "api/collections/" + accountUser } func (c *Collection) RenderMathJax() bool { return c.db.CollectionHasAttribute(c.ID, "render_mathjax") } func (c *Collection) EmailSubsEnabled() bool { return c.db.CollectionHasAttribute(c.ID, "email_subs") } func (c *Collection) MonetizationURL() string { if c.Monetization == "" { return "" } return strings.Replace(c.Monetization, "$", "https://", 1) } // DisplayDescription returns the description with rendered Markdown and HTML. func (c *Collection) DisplayDescription() *template.HTML { if c.Description == "" { s := template.HTML("") return &s } t := template.HTML(posts.ApplyBasicAccessibleMarkdown([]byte(c.Description))) return &t } // PlainDescription returns the description with all Markdown and HTML removed. func (c *Collection) PlainDescription() string { if c.Description == "" { return "" } desc := stripHTMLWithoutEscaping(c.Description) desc = stripmd.Strip(desc) return desc } func (c CollectionPage) DisplayMonetization() string { return displayMonetization(c.Monetization, c.Alias) } func (c *DisplayCollection) Direction() string { if c.Language == "" { return "auto" } if i18n.LangIsRTL(c.Language) { return "rtl" } return "ltr" } func newCollection(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r) alias := r.FormValue("alias") title := r.FormValue("title") var missingParams, accessToken string var u *User c := struct { Alias string `json:"alias" schema:"alias"` Title string `json:"title" schema:"title"` Web bool `json:"web" schema:"web"` }{} if reqJSON { // Decode JSON request decoder := json.NewDecoder(r.Body) err := decoder.Decode(&c) if err != nil { log.Error("Couldn't parse post update JSON request: %v\n", err) return ErrBadJSON } } else { // TODO: move form parsing to formDecoder c.Alias = alias c.Title = title } if c.Alias == "" { if c.Title != "" { // If only a title was given, just use it to generate the alias. c.Alias = getSlug(c.Title, "") } else { missingParams += "`alias` " } } if c.Title == "" { missingParams += "`title` " } if missingParams != "" { return impart.HTTPError{http.StatusBadRequest, fmt.Sprintf("Parameter(s) %srequired.", missingParams)} } var userID int64 var err error if reqJSON && !c.Web { accessToken = r.Header.Get("Authorization") if accessToken == "" { return ErrNoAccessToken } userID = app.db.GetUserID(accessToken) if userID == -1 { return ErrBadAccessToken } } else { u = getUserSession(app, r) if u == nil { return ErrNotLoggedIn } userID = u.ID } silenced, err := app.db.IsUserSilenced(userID) if err != nil { log.Error("new collection: %v", err) return ErrInternalGeneral } if silenced { return ErrUserSilenced } if !author.IsValidUsername(app.cfg, c.Alias) { return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."} } coll, err := app.db.CreateCollection(app.cfg, c.Alias, c.Title, userID) if err != nil { // TODO: handle this return err } res := &CollectionObj{Collection: *coll} if reqJSON { return impart.WriteSuccess(w, res, http.StatusCreated) } redirectTo := "/me/c/" // TODO: redirect to pad when necessary return impart.HTTPError{http.StatusFound, redirectTo} } func apiCheckCollectionPermissions(app *App, r *http.Request, c *Collection) (int64, error) { accessToken := r.Header.Get("Authorization") var userID int64 = -1 if accessToken != "" { userID = app.db.GetUserID(accessToken) } isCollOwner := userID == c.OwnerID if c.IsPrivate() && !isCollOwner { // Collection is private, but user isn't authenticated return -1, ErrCollectionNotFound } if c.IsProtected() { // TODO: check access token return -1, ErrCollectionUnauthorizedRead } return userID, nil } // fetchCollection handles the API endpoint for retrieving collection data. func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error { if IsActivityPubRequest(r) { return handleFetchCollectionActivities(app, w, r) } vars := mux.Vars(r) alias := vars["alias"] // TODO: move this logic into a common getCollection function // Get base Collection data c, err := app.db.GetCollection(alias) if err != nil { return err } c.hostName = app.cfg.App.Host // Redirect users who aren't requesting JSON reqJSON := IsJSON(r) if !reqJSON { return impart.HTTPError{http.StatusFound, c.CanonicalURL()} } // Check permissions userID, err := apiCheckCollectionPermissions(app, r, c) if err != nil { return err } isCollOwner := userID == c.OwnerID // Fetch extra data about the Collection res := &CollectionObj{Collection: *c} if c.PublicOwner { u, err := app.db.GetUserByID(res.OwnerID) if err != nil { // Log the error and just continue log.Error("Error getting user for collection: %v", err) } else { res.Owner = u } } // TODO: check status for silenced app.db.GetPostsCount(res, isCollOwner) // Strip non-public information res.Collection.ForPublic() return impart.WriteSuccess(w, res, http.StatusOK) } // fetchCollectionPosts handles an API endpoint for retrieving a collection's // posts. func fetchCollectionPosts(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) alias := vars["alias"] c, err := app.db.GetCollection(alias) if err != nil { return err } c.hostName = app.cfg.App.Host // Check permissions userID, err := apiCheckCollectionPermissions(app, r, c) if err != nil { return err } isCollOwner := userID == c.OwnerID // Get page page := 1 if p := r.FormValue("page"); p != "" { pInt, _ := strconv.Atoi(p) if pInt > 0 { page = pInt } } ps, err := app.db.GetPosts(app.cfg, c, page, isCollOwner, false, false, "") if err != nil { return err } coll := &CollectionObj{Collection: *c, Posts: ps} app.db.GetPostsCount(coll, isCollOwner) // Strip non-public information coll.Collection.ForPublic() // Transform post bodies if needed if r.FormValue("body") == "html" { for _, p := range *coll.Posts { p.Content = posts.ApplyMarkdown([]byte(p.Content)) } } return impart.WriteSuccess(w, coll, http.StatusOK) } type CollectionPage struct { page.StaticPage *DisplayCollection IsCustomDomain bool IsWelcome bool IsOwner bool IsCollLoggedIn bool Honeypot string IsSubscriber bool CanPin bool Username string Monetization string FediverseAuthor string Flash template.HTML Collections *[]Collection PinnedPosts *[]PublicPost IsAdmin bool CanInvite bool // Helper field for Chorus mode CollAlias string } type TagCollectionPage struct { CollectionPage Tag string } func (tcp TagCollectionPage) PrevPageURL(prefix string, n int, tl bool) string { - u := fmt.Sprintf("/tag:%s", tcp.Tag) + u := fmt.Sprintf("tag:%s", tcp.Tag) if n > 2 { u += fmt.Sprintf("/page/%d", n-1) } if tl { return u } - return "/" + prefix + tcp.Alias + u + return prefix + tcp.Alias + "/" + u } func (tcp TagCollectionPage) NextPageURL(prefix string, n int, tl bool) string { if tl { - return fmt.Sprintf("/tag:%s/page/%d", tcp.Tag, n+1) + return fmt.Sprintf("tag:%s/page/%d", tcp.Tag, n+1) } - return fmt.Sprintf("/%s%s/tag:%s/page/%d", prefix, tcp.Alias, tcp.Tag, n+1) + return fmt.Sprintf("%s%s/tag:%s/page/%d", prefix, tcp.Alias, tcp.Tag, n+1) } func NewCollectionObj(c *Collection) *CollectionObj { return &CollectionObj{ Collection: *c, Format: c.NewFormat(), } } func (c *CollectionObj) ScriptDisplay() template.JS { return template.JS(c.Script) } var jsSourceCommentReg = regexp.MustCompile("(?m)^// src:(.+)$") func (c *CollectionObj) ExternalScripts() []template.URL { scripts := []template.URL{} if c.Script == "" { return scripts } matches := jsSourceCommentReg.FindAllStringSubmatch(c.Script, -1) for _, m := range matches { scripts = append(scripts, template.URL(strings.TrimSpace(m[1]))) } return scripts } func (c *CollectionObj) CanShowScript() bool { return false } func processCollectionRequest(cr *collectionReq, vars map[string]string, w http.ResponseWriter, r *http.Request) error { cr.prefix = vars["prefix"] cr.alias = vars["collection"] // Normalize the URL, redirecting user to consistent post URL if cr.alias != strings.ToLower(cr.alias) { return impart.HTTPError{http.StatusMovedPermanently, fmt.Sprintf("/%s/", strings.ToLower(cr.alias))} } return nil } // processCollectionPermissions checks the permissions for the given // collectionReq, returning a Collection if access is granted; otherwise this // renders any necessary collection pages, for example, if requesting a custom // domain that doesn't yet have a collection associated, or if a collection // requires a password. In either case, this will return nil, nil -- thus both // values should ALWAYS be checked to determine whether or not to continue. func processCollectionPermissions(app *App, cr *collectionReq, u *User, w http.ResponseWriter, r *http.Request) (*Collection, error) { // Display collection if this is a collection var c *Collection var err error if app.cfg.App.SingleUser { c, err = app.db.GetCollectionByID(1) } else { c, err = app.db.GetCollection(cr.alias) } // TODO: verify we don't reveal the existence of a private collection with redirection if err != nil { if err, ok := err.(impart.HTTPError); ok { if err.Status == http.StatusNotFound { if cr.isCustomDomain { // User is on the site from a custom domain //tErr := pages["404-domain.tmpl"].ExecuteTemplate(w, "base", pageForHost(page.StaticPage{}, r)) //if tErr != nil { //log.Error("Unable to render 404-domain page: %v", err) //} return nil, nil } if len(cr.alias) >= minIDLen && len(cr.alias) <= maxIDLen { // Alias is within post ID range, so just be sure this isn't a post if app.db.PostIDExists(cr.alias) { // TODO: use StatusFound for vanity post URLs when we implement them return nil, impart.HTTPError{http.StatusMovedPermanently, "/" + cr.alias} } } // Redirect if necessary newAlias := app.db.GetCollectionRedirect(cr.alias) if newAlias != "" { return nil, impart.HTTPError{http.StatusFound, "/" + newAlias + "/"} } } } return nil, err } c.hostName = app.cfg.App.Host // Update CollectionRequest to reflect owner status cr.isCollOwner = u != nil && u.ID == c.OwnerID // Check permissions if !cr.isCollOwner { if c.IsPrivate() { return nil, ErrCollectionNotFound } else if c.IsProtected() { uname := "" if u != nil { uname = u.Username } // TODO: move this to all permission checks? suspended, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("process protected collection permissions: %v", err) return nil, err } if suspended { return nil, ErrCollectionNotFound } // See if we've authorized this collection cr.isAuthorized = isAuthorizedForCollection(app, c.Alias, r) if !cr.isAuthorized { p := struct { page.StaticPage *CollectionObj Username string Next string Flashes []template.HTML }{ StaticPage: pageForReq(app, r), CollectionObj: &CollectionObj{Collection: *c}, Username: uname, Next: r.FormValue("g"), Flashes: []template.HTML{}, } // Get owner information p.CollectionObj.Owner, err = app.db.GetUserByID(c.OwnerID) if err != nil { // Log the error and just continue log.Error("Error getting user for collection: %v", err) } flashes, _ := getSessionFlashes(app, w, r, nil) for _, flash := range flashes { p.Flashes = append(p.Flashes, template.HTML(flash)) } err = templates["password-collection"].ExecuteTemplate(w, "password-collection", p) if err != nil { log.Error("Unable to render password-collection: %v", err) return nil, err } return nil, nil } } } return c, nil } func checkUserForCollection(app *App, cr *collectionReq, r *http.Request, isPostReq bool) (*User, error) { u := getUserSession(app, r) return u, nil } func newDisplayCollection(c *Collection, cr *collectionReq, page int) (*DisplayCollection, error) { coll := &DisplayCollection{ CollectionObj: NewCollectionObj(c), CurrentPage: page, Prefix: cr.prefix, IsTopLevel: isSingleUser, } err := c.db.GetPostsCount(coll.CollectionObj, cr.isCollOwner) if err != nil { return nil, err } return coll, nil } // getCollectionPage returns the collection page as an int. If the parsed page value is not // greater than 0 then the default value of 1 is returned. func getCollectionPage(vars map[string]string) int { if p, _ := strconv.Atoi(vars["page"]); p > 0 { return p } return 1 } // handleViewCollection displays the requested Collection func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) cr := &collectionReq{} err := processCollectionRequest(cr, vars, w, r) if err != nil { return err } u, err := checkUserForCollection(app, cr, r, false) if err != nil { return err } page := getCollectionPage(vars) c, err := processCollectionPermissions(app, cr, u, w, r) if c == nil || err != nil { return err } c.hostName = app.cfg.App.Host silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("view collection: %v", err) return ErrInternalGeneral } // Serve ActivityStreams data now, if requested if IsActivityPubRequest(r) { ac := c.PersonObject() setCacheControl(w, apCacheTime) return impart.RenderActivityJSON(w, ac, http.StatusOK) } // Fetch extra data about the Collection // TODO: refactor out this logic, shared in collection.go:fetchCollection() coll, err := newDisplayCollection(c, cr, page) if err != nil { return err } var ct PostType if isArchiveView(r) { ct = postArch } // FIXME: this number will be off when user has pinned posts but isn't a Pro user ppp := coll.Format.PostsPerPage() if ct == postArch { ppp = postsPerArchPage } coll.TotalPages = int(math.Ceil(float64(coll.TotalPosts) / float64(ppp))) if coll.TotalPages > 0 && page > coll.TotalPages { - redirURL := fmt.Sprintf("/page/%d", coll.TotalPages) + redirURL := fmt.Sprintf("{{subdir}}/page/%d", coll.TotalPages) if !app.cfg.App.SingleUser { - redirURL = fmt.Sprintf("/%s%s%s", cr.prefix, coll.Alias, redirURL) + redirURL = fmt.Sprintf("{{subdir}}/%s%s%s", cr.prefix, coll.Alias, redirURL) } return impart.HTTPError{http.StatusFound, redirURL} } coll.Posts, _ = app.db.GetPosts(app.cfg, c, page, cr.isCollOwner, false, false, "") // Serve collection displayPage := CollectionPage{ DisplayCollection: coll, IsCollLoggedIn: cr.isAuthorized, StaticPage: pageForReq(app, r), IsCustomDomain: cr.isCustomDomain, IsWelcome: r.FormValue("greeting") != "", Honeypot: spam.HoneypotFieldName(), CollAlias: c.Alias, } flashes, _ := getSessionFlashes(app, w, r, nil) for _, f := range flashes { displayPage.Flash = template.HTML(f) } displayPage.IsAdmin = u != nil && u.IsAdmin() displayPage.CanInvite = canUserInvite(app.cfg, displayPage.IsAdmin) var owner *User if u != nil { displayPage.Username = u.Username displayPage.IsOwner = u.ID == coll.OwnerID displayPage.IsSubscriber = u.IsEmailSubscriber(app, coll.ID) if displayPage.IsOwner { // Add in needed information for users viewing their own collection owner = u displayPage.CanPin = true pubColls, err := app.db.GetPublishableCollections(owner, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } displayPage.Collections = pubColls } } isOwner := owner != nil if !isOwner { // Current user doesn't own collection; retrieve owner information owner, err = app.db.GetUserByID(coll.OwnerID) if err != nil { // Log the error and just continue log.Error("Error getting user for collection: %v", err) } } if !isOwner && silenced { return ErrCollectionNotFound } displayPage.Silenced = isOwner && silenced displayPage.Owner = owner coll.Owner = displayPage.Owner // Add more data // TODO: fix this mess of collections inside collections displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner) displayPage.Monetization = app.db.GetCollectionAttribute(coll.ID, "monetization_pointer") collTmpl := "collection" if app.cfg.App.Chorus { collTmpl = "chorus-collection" } else if isArchiveView(r) { displayPage.NavSuffix = "/archive/" collTmpl = "collection-archive" } err = templates[collTmpl].ExecuteTemplate(w, "collection", displayPage) if err != nil { log.Error("Unable to render collection index: %v", err) } // Update collection view count go func() { // Don't update if owner is viewing the collection. if u != nil && u.ID == coll.OwnerID { return } // Only update for human views if r.Method == "HEAD" || bots.IsBot(r.UserAgent()) { return } _, err := app.db.Exec("UPDATE collections SET view_count = view_count + 1 WHERE id = ?", coll.ID) if err != nil { log.Error("Unable to update collections count: %v", err) } }() return err } func isArchiveView(r *http.Request) bool { return strings.HasSuffix(r.RequestURI, "/archive/") || mux.Vars(r)["archive"] == "archive" } func handleViewMention(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) handle := vars["handle"] remoteUser, err := app.db.GetProfilePageFromHandle(app, handle) if err != nil || remoteUser == "" { log.Error("Couldn't find user %s: %v", handle, err) return ErrRemoteUserNotFound } return impart.HTTPError{Status: http.StatusFound, Message: remoteUser} } func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) tag := vars["tag"] cr := &collectionReq{} err := processCollectionRequest(cr, vars, w, r) if err != nil { return err } u, err := checkUserForCollection(app, cr, r, false) if err != nil { return err } page := getCollectionPage(vars) c, err := processCollectionPermissions(app, cr, u, w, r) if c == nil || err != nil { return err } coll, _ := newDisplayCollection(c, cr, page) taggedPostIDs, err := app.db.GetAllPostsTaggedIDs(c, tag, cr.isCollOwner) if err != nil { return err } ttlPosts := len(taggedPostIDs) pagePosts := coll.Format.PostsPerPage() coll.TotalPages = int(math.Ceil(float64(ttlPosts) / float64(pagePosts))) if coll.TotalPages > 0 && page > coll.TotalPages { - redirURL := fmt.Sprintf("/page/%d", coll.TotalPages) + redirURL := fmt.Sprintf("{{subdir}}/tag:%s/page/%d", tag, coll.TotalPages) if !app.cfg.App.SingleUser { - redirURL = fmt.Sprintf("/%s%s%s", cr.prefix, coll.Alias, redirURL) + redirURL = fmt.Sprintf("{{subdir}}/%s%s%s", cr.prefix, coll.Alias, redirURL) } return impart.HTTPError{http.StatusFound, redirURL} } coll.Posts, _ = app.db.GetPostsTagged(app.cfg, c, tag, page, cr.isCollOwner) if coll.Posts != nil && len(*coll.Posts) == 0 { return ErrCollectionPageNotFound } // Serve collection displayPage := TagCollectionPage{ CollectionPage: CollectionPage{ DisplayCollection: coll, StaticPage: pageForReq(app, r), IsCustomDomain: cr.isCustomDomain, }, Tag: tag, } var owner *User if u != nil { displayPage.Username = u.Username displayPage.IsOwner = u.ID == coll.OwnerID if displayPage.IsOwner { // Add in needed information for users viewing their own collection owner = u displayPage.CanPin = true pubColls, err := app.db.GetPublishableCollections(owner, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } displayPage.Collections = pubColls } } isOwner := owner != nil if !isOwner { // Current user doesn't own collection; retrieve owner information owner, err = app.db.GetUserByID(coll.OwnerID) if err != nil { // Log the error and just continue log.Error("Error getting user for collection: %v", err) } if owner.IsSilenced() { return ErrCollectionNotFound } } displayPage.Silenced = owner != nil && owner.IsSilenced() displayPage.Owner = owner coll.Owner = displayPage.Owner // Add more data // TODO: fix this mess of collections inside collections displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner) displayPage.Monetization = app.db.GetCollectionAttribute(coll.ID, "monetization_pointer") err = templates["collection-tags"].ExecuteTemplate(w, "collection-tags", displayPage) if err != nil { log.Error("Unable to render collection tag page: %v", err) } return nil } func handleViewCollectionLang(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) lang := vars["lang"] cr := &collectionReq{} err := processCollectionRequest(cr, vars, w, r) if err != nil { return err } u, err := checkUserForCollection(app, cr, r, false) if err != nil { return err } page := getCollectionPage(vars) c, err := processCollectionPermissions(app, cr, u, w, r) if c == nil || err != nil { return err } coll, _ := newDisplayCollection(c, cr, page) coll.Language = lang coll.NavSuffix = fmt.Sprintf("/lang:%s", lang) ttlPosts, err := app.db.GetCollLangTotalPosts(coll.ID, lang) if err != nil { log.Error("Unable to getCollLangTotalPosts: %s", err) } pagePosts := coll.Format.PostsPerPage() coll.TotalPages = int(math.Ceil(float64(ttlPosts) / float64(pagePosts))) if coll.TotalPages > 0 && page > coll.TotalPages { - redirURL := fmt.Sprintf("/lang:%s/page/%d", lang, coll.TotalPages) + redirURL := fmt.Sprintf("{{subdir}}/lang:%s/page/%d", lang, coll.TotalPages) if !app.cfg.App.SingleUser { - redirURL = fmt.Sprintf("/%s%s%s", cr.prefix, coll.Alias, redirURL) + redirURL = fmt.Sprintf("{{subdir}}/%s%s%s", cr.prefix, coll.Alias, redirURL) } return impart.HTTPError{http.StatusFound, redirURL} } coll.Posts, _ = app.db.GetLangPosts(app.cfg, c, lang, page, cr.isCollOwner) if err != nil { return ErrCollectionPageNotFound } // Serve collection displayPage := struct { CollectionPage Tag string }{ CollectionPage: CollectionPage{ DisplayCollection: coll, StaticPage: pageForReq(app, r), IsCustomDomain: cr.isCustomDomain, }, Tag: lang, } var owner *User if u != nil { displayPage.Username = u.Username displayPage.IsOwner = u.ID == coll.OwnerID if displayPage.IsOwner { // Add in needed information for users viewing their own collection owner = u displayPage.CanPin = true pubColls, err := app.db.GetPublishableCollections(owner, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } displayPage.Collections = pubColls } } isOwner := owner != nil if !isOwner { // Current user doesn't own collection; retrieve owner information owner, err = app.db.GetUserByID(coll.OwnerID) if err != nil { // Log the error and just continue log.Error("Error getting user for collection: %v", err) } if owner.IsSilenced() { return ErrCollectionNotFound } } displayPage.Silenced = owner != nil && owner.IsSilenced() displayPage.Owner = owner coll.Owner = displayPage.Owner // Add more data // TODO: fix this mess of collections inside collections displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner) displayPage.Monetization = app.db.GetCollectionAttribute(coll.ID, "monetization_pointer") collTmpl := "collection" if app.cfg.App.Chorus { collTmpl = "chorus-collection" } err = templates[collTmpl].ExecuteTemplate(w, "collection", displayPage) if err != nil { log.Error("Unable to render collection lang page: %v", err) } return nil } func handleCollectionPostRedirect(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) slug := vars["slug"] cr := &collectionReq{} err := processCollectionRequest(cr, vars, w, r) if err != nil { return err } // Normalize the URL, redirecting user to consistent post URL loc := fmt.Sprintf("/%s", slug) if !app.cfg.App.SingleUser { loc = fmt.Sprintf("/%s/%s", cr.alias, slug) } return impart.HTTPError{http.StatusFound, loc} } func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r) vars := mux.Vars(r) collAlias := vars["alias"] isWeb := r.FormValue("web") == "1" u := &User{} if reqJSON && !isWeb { // Ensure an access token was given accessToken := r.Header.Get("Authorization") u.ID = app.db.GetUserID(accessToken) if u.ID == -1 { return ErrBadAccessToken } } else { u = getUserSession(app, r) if u == nil { return ErrNotLoggedIn } } silenced, err := app.db.IsUserSilenced(u.ID) if err != nil { log.Error("existing collection: %v", err) return ErrInternalGeneral } if silenced { return ErrUserSilenced } if r.Method == "DELETE" { err := app.db.DeleteCollection(collAlias, u.ID) if err != nil { // TODO: if not HTTPError, report error to admin log.Error("Unable to delete collection: %s", err) return err } addSessionFlash(app, w, r, "Deleted your blog, "+collAlias+".", nil) return impart.HTTPError{Status: http.StatusNoContent} } c := SubmittedCollection{OwnerID: uint64(u.ID)} if reqJSON { // Decode JSON request decoder := json.NewDecoder(r.Body) err = decoder.Decode(&c) if err != nil { log.Error("Couldn't parse collection update JSON request: %v\n", err) return ErrBadJSON } } else { err = r.ParseForm() if err != nil { log.Error("Couldn't parse collection update form request: %v\n", err) return ErrBadFormData } err = app.formDecoder.Decode(&c, r.PostForm) if err != nil { log.Error("Couldn't decode collection update form request: %v\n", err) return ErrBadFormData } } err = app.db.UpdateCollection(app, &c, collAlias) if err != nil { if err, ok := err.(impart.HTTPError); ok { if reqJSON { return err } addSessionFlash(app, w, r, err.Message, nil) return impart.HTTPError{http.StatusFound, "/me/c/" + collAlias} } else { log.Error("Couldn't update collection: %v\n", err) return err } } + // Federate profile update to followers so remote servers (e.g. Mastodon) + // pick up bio / title changes immediately. + if coll, ferr := app.db.GetCollection(collAlias); ferr == nil { + if ferr = federateActor(app, coll); ferr != nil { + log.Error("Couldn't federate actor update: %v", ferr) + } + } + if reqJSON { return impart.WriteSuccess(w, struct { }{}, http.StatusOK) } addSessionFlash(app, w, r, "Blog updated!", nil) return impart.HTTPError{http.StatusFound, "/me/c/" + collAlias} } // collectionAliasFromReq takes a request and returns the collection alias // if it can be ascertained, as well as whether or not the collection uses a // custom domain. func collectionAliasFromReq(r *http.Request) string { vars := mux.Vars(r) alias := vars["subdomain"] isSubdomain := alias != "" if !isSubdomain { // Fall back to write.as/{collection} since this isn't a custom domain alias = vars["collection"] } return alias } func handleWebCollectionUnlock(app *App, w http.ResponseWriter, r *http.Request) error { var readReq struct { Alias string `schema:"alias" json:"alias"` Pass string `schema:"password" json:"password"` Next string `schema:"to" json:"to"` } // Get params if impart.ReqJSON(r) { decoder := json.NewDecoder(r.Body) err := decoder.Decode(&readReq) if err != nil { log.Error("Couldn't parse readReq JSON request: %v\n", err) return ErrBadJSON } } else { err := r.ParseForm() if err != nil { log.Error("Couldn't parse readReq form request: %v\n", err) return ErrBadFormData } err = app.formDecoder.Decode(&readReq, r.PostForm) if err != nil { log.Error("Couldn't decode readReq form request: %v\n", err) return ErrBadFormData } } if readReq.Alias == "" { return impart.HTTPError{http.StatusBadRequest, "Need a collection `alias` to read."} } if readReq.Pass == "" { return impart.HTTPError{http.StatusBadRequest, "Please supply a password."} } var collHashedPass []byte err := app.db.QueryRow("SELECT password FROM collectionpasswords INNER JOIN collections ON id = collection_id WHERE alias = ?", readReq.Alias).Scan(&collHashedPass) if err != nil { if err == sql.ErrNoRows { log.Error("No collectionpassword found when trying to read collection %s", readReq.Alias) return impart.HTTPError{http.StatusInternalServerError, "Something went very wrong. The humans have been alerted."} } return err } if !auth.Authenticated(collHashedPass, []byte(readReq.Pass)) { return impart.HTTPError{http.StatusUnauthorized, "Incorrect password."} } // Success; set cookie session, err := app.sessionStore.Get(r, blogPassCookieName) if err == nil { session.Values[readReq.Alias] = true err = session.Save(r, w) if err != nil { log.Error("Didn't save unlocked blog '%s': %v", readReq.Alias, err) } } next := "/" + readReq.Next if !app.cfg.App.SingleUser { next = "/" + readReq.Alias + next } return impart.HTTPError{http.StatusFound, next} } func isAuthorizedForCollection(app *App, alias string, r *http.Request) bool { authd := false session, err := app.sessionStore.Get(r, blogPassCookieName) if err == nil { _, authd = session.Values[alias] } return authd } func logOutCollection(app *App, alias string, w http.ResponseWriter, r *http.Request) error { session, err := app.sessionStore.Get(r, blogPassCookieName) if err != nil { return err } // Remove this from map of blogs logged into delete(session.Values, alias) // If not auth'd with any blog, delete entire cookie if len(session.Values) == 0 { session.Options.MaxAge = -1 } return session.Save(r, w) } func handleLogOutCollection(app *App, w http.ResponseWriter, r *http.Request) error { alias := collectionAliasFromReq(r) var c *Collection var err error if app.cfg.App.SingleUser { c, err = app.db.GetCollectionByID(1) } else { c, err = app.db.GetCollection(alias) } if err != nil { return err } if !c.IsProtected() { // Invalid to log out of this collection return ErrCollectionPageNotFound } err = logOutCollection(app, c.Alias, w, r) if err != nil { addSessionFlash(app, w, r, "Logging out failed. Try clearing cookies for this site, instead.", nil) } return impart.HTTPError{http.StatusFound, c.CanonicalURL()} } diff --git a/collections_subdir_test.go b/collections_subdir_test.go new file mode 100644 index 0000000..856fe3c --- /dev/null +++ b/collections_subdir_test.go @@ -0,0 +1,43 @@ +package writefreely + +import "testing" + +func TestCollectionCanonicalURLIncludesSubdirectoryForAppHost(t *testing.T) { + prevHost := canonicalAppHost + prevSubdir := canonicalSubdir + defer func() { + canonicalAppHost = prevHost + canonicalSubdir = prevSubdir + }() + + canonicalAppHost = "https://example.com" + canonicalSubdir = "/blog" + isSingleUser = false + + c := &Collection{Alias: "myblog", hostName: "https://example.com"} + got := c.CanonicalURL() + want := "https://example.com/blog/myblog/" + if got != want { + t.Fatalf("unexpected canonical URL: got %q want %q", got, want) + } +} + +func TestCollectionCanonicalURLDoesNotAlterCustomDomain(t *testing.T) { + prevHost := canonicalAppHost + prevSubdir := canonicalSubdir + defer func() { + canonicalAppHost = prevHost + canonicalSubdir = prevSubdir + }() + + canonicalAppHost = "https://example.com" + canonicalSubdir = "/blog" + isSingleUser = false + + c := &Collection{Alias: "myblog", hostName: "https://blog.example.net"} + got := c.CanonicalURL() + want := "https://blog.example.net/myblog/" + if got != want { + t.Fatalf("unexpected canonical URL for custom domain: got %q want %q", got, want) + } +} diff --git a/config/config.go b/config/config.go index 7bee863..7425b44 100644 --- a/config/config.go +++ b/config/config.go @@ -1,315 +1,399 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ // Package config holds and assists in the configuration of a writefreely instance. package config import ( "net/url" "strings" "github.com/go-ini/ini" "github.com/writeas/web-core/log" "golang.org/x/net/idna" ) const ( // FileName is the default configuration file name FileName = "config.ini" UserNormal UserType = "user" UserAdmin = "admin" ) type ( UserType string // ServerCfg holds values that affect how the HTTP server runs ServerCfg struct { HiddenHost string `ini:"hidden_host"` Port int `ini:"port"` Bind string `ini:"bind"` TLSCertPath string `ini:"tls_cert_path"` TLSKeyPath string `ini:"tls_key_path"` Autocert bool `ini:"autocert"` TemplatesParentDir string `ini:"templates_parent_dir"` StaticParentDir string `ini:"static_parent_dir"` PagesParentDir string `ini:"pages_parent_dir"` KeysParentDir string `ini:"keys_parent_dir"` HashSeed string `ini:"hash_seed"` GopherPort int `ini:"gopher_port"` Dev bool `ini:"-"` } // DatabaseCfg holds values that determine how the application connects to a datastore DatabaseCfg struct { Type string `ini:"type"` FileName string `ini:"filename"` User string `ini:"username"` Password string `ini:"password"` Database string `ini:"database"` Host string `ini:"host"` Port int `ini:"port"` TLS bool `ini:"tls"` } WriteAsOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` AuthLocation string `ini:"auth_location"` TokenLocation string `ini:"token_location"` InspectLocation string `ini:"inspect_location"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` } GitlabOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` Host string `ini:"host"` DisplayName string `ini:"display_name"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` } GiteaOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` Host string `ini:"host"` DisplayName string `ini:"display_name"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` } SlackOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` TeamID string `ini:"team_id"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` } GenericOauthCfg struct { ClientID string `ini:"client_id"` ClientSecret string `ini:"client_secret"` Host string `ini:"host"` DisplayName string `ini:"display_name"` CallbackProxy string `ini:"callback_proxy"` CallbackProxyAPI string `ini:"callback_proxy_api"` TokenEndpoint string `ini:"token_endpoint"` InspectEndpoint string `ini:"inspect_endpoint"` AuthEndpoint string `ini:"auth_endpoint"` Scope string `ini:"scope"` AllowDisconnect bool `ini:"allow_disconnect"` MapUserID string `ini:"map_user_id"` MapUsername string `ini:"map_username"` MapDisplayName string `ini:"map_display_name"` MapEmail string `ini:"map_email"` } // AppCfg holds values that affect how the application functions AppCfg struct { - SiteName string `ini:"site_name"` - SiteDesc string `ini:"site_description"` - Host string `ini:"host"` + SiteName string `ini:"site_name"` + SiteDesc string `ini:"site_description"` + Host string `ini:"host"` + Subdirectory string `ini:"subdirectory"` // Site appearance Theme string `ini:"theme"` Editor string `ini:"editor"` JSDisabled bool `ini:"disable_js"` WebFonts bool `ini:"webfonts"` Landing string `ini:"landing"` SimpleNav bool `ini:"simple_nav"` WFModesty bool `ini:"wf_modesty"` // Site functionality Chorus bool `ini:"chorus"` Forest bool `ini:"forest"` // The admin cares about the forest, not the trees. Hide unnecessary technical info. DisableDrafts bool `ini:"disable_drafts"` // Users SingleUser bool `ini:"single_user"` OpenRegistration bool `ini:"open_registration"` OpenDeletion bool `ini:"open_deletion"` MinUsernameLen int `ini:"min_username_len"` MaxBlogs int `ini:"max_blogs"` // Options for public instances // Federation Federation bool `ini:"federation"` PublicStats bool `ini:"public_stats"` Monetization bool `ini:"monetization"` NotesOnly bool `ini:"notes_only"` // Access Private bool `ini:"private"` // Additional functions LocalTimeline bool `ini:"local_timeline"` UserInvites string `ini:"user_invites"` // Defaults DefaultVisibility string `ini:"default_visibility"` // Check for Updates UpdateChecks bool `ini:"update_checks"` // Disable password authentication if use only Oauth DisablePasswordAuth bool `ini:"disable_password_auth"` } EmailCfg struct { // SMTP configuration values Host string `ini:"smtp_host"` Port int `ini:"smtp_port"` Username string `ini:"smtp_username"` Password string `ini:"smtp_password"` + EnableSSL bool `ini:"smtp_ssl"` EnableStartTLS bool `ini:"smtp_enable_start_tls"` // Mailgun configuration values Domain string `ini:"domain"` MailgunPrivate string `ini:"mailgun_private"` MailgunEurope bool `ini:"mailgun_europe"` } // Config holds the complete configuration for running a writefreely instance Config struct { Server ServerCfg `ini:"server"` Database DatabaseCfg `ini:"database"` App AppCfg `ini:"app"` Email EmailCfg `ini:"email"` SlackOauth SlackOauthCfg `ini:"oauth.slack"` WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"` GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"` GiteaOauth GiteaOauthCfg `ini:"oauth.gitea"` GenericOauth GenericOauthCfg `ini:"oauth.generic"` } ) // New creates a new Config with sane defaults func New() *Config { c := &Config{ Server: ServerCfg{ Port: 8080, Bind: "localhost", /* IPV6 support when not using localhost? */ }, App: AppCfg{ Host: "http://localhost:8080", Theme: "write", WebFonts: true, SingleUser: true, MinUsernameLen: 3, MaxBlogs: 1, Federation: true, PublicStats: true, }, } c.UseMySQL(true) return c } // UseMySQL resets the Config's Database to use default values for a MySQL setup. func (cfg *Config) UseMySQL(fresh bool) { cfg.Database.Type = "mysql" if fresh { cfg.Database.Host = "localhost" cfg.Database.Port = 3306 } } // UseSQLite resets the Config's Database to use default values for a SQLite setup. func (cfg *Config) UseSQLite(fresh bool) { cfg.Database.Type = "sqlite3" if fresh { cfg.Database.FileName = "writefreely.db" } } // IsSecureStandalone returns whether or not the application is running as a // standalone server with TLS enabled. func (cfg *Config) IsSecureStandalone() bool { return cfg.Server.Port == 443 && cfg.Server.TLSCertPath != "" && cfg.Server.TLSKeyPath != "" } func (ac *AppCfg) LandingPath() string { if !strings.HasPrefix(ac.Landing, "/") { - return "/" + ac.Landing + return ac.PrefixPath("/" + ac.Landing) + } + return ac.PrefixPath(ac.Landing) +} + +func (ac AppCfg) SubdirectoryPath() string { + p := strings.TrimSpace(ac.Subdirectory) + if p == "" || p == "/" { + return "" + } + if !strings.HasPrefix(p, "/") { + p = "/" + p + } + return strings.TrimSuffix(p, "/") +} + +func (ac AppCfg) PrefixPath(path string) string { + if path == "" { + path = "/" + } + if strings.Contains(path, "://") || strings.HasPrefix(path, "//") { + return path + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + basePath := ac.SubdirectoryPath() + if basePath == "" || path == basePath || strings.HasPrefix(path, basePath+"/") { + return path + } + if path == "/" { + return basePath + "/" + } + return basePath + path +} + +func (ac AppCfg) AbsoluteHost() string { + host := strings.TrimSuffix(strings.TrimSpace(ac.Host), "/") + if host == "" { + return "" + } + if basePath := ac.SubdirectoryPath(); basePath != "" && !strings.HasSuffix(host, basePath) { + host += basePath } - return ac.Landing + return host +} + +func (ac AppCfg) AbsoluteURL(path string) string { + host := strings.TrimSuffix(strings.TrimSpace(ac.Host), "/") + if host == "" { + return ac.PrefixPath(path) + } + return host + ac.PrefixPath(path) +} + +func (ac AppCfg) StripSubdirectory(path string) string { + basePath := ac.SubdirectoryPath() + if basePath == "" { + if path == "" { + return "/" + } + return path + } + if path == basePath { + return "/" + } + if strings.HasPrefix(path, basePath+"/") { + return strings.TrimPrefix(path, basePath) + } + if path == "" { + return "/" + } + return path } func (lc EmailCfg) Enabled() bool { return (lc.Domain != "" && lc.MailgunPrivate != "") || lc.Username != "" && lc.Password != "" && lc.Host != "" && lc.Port > 0 } func (ac AppCfg) SignupPath() string { if !ac.OpenRegistration { return "" } if ac.Chorus || ac.Private || (ac.Landing != "" && ac.Landing != "/") { - return "/signup" + return ac.PrefixPath("/signup") } - return "/" + return ac.PrefixPath("/") } // Load reads the given configuration file, then parses and returns it as a Config. func Load(fname string) (*Config, error) { if fname == "" { fname = FileName } cfg, err := ini.Load(fname) if err != nil { return nil, err } // Parse INI file uc := &Config{} err = cfg.MapTo(uc) if err != nil { return nil, err } // Do any transformations u, err := url.Parse(uc.App.Host) if err != nil { return nil, err } d, err := idna.ToASCII(u.Hostname()) if err != nil { log.Error("idna.ToASCII for %s: %s", u.Hostname(), err) return nil, err } uc.App.Host = u.Scheme + "://" + d if u.Port() != "" { uc.App.Host += ":" + u.Port() } + // Keep host as scheme://domain[:port] only, and treat any host path as + // a subdirectory setting (unless an explicit subdirectory already differs). + hostPath := strings.TrimSuffix(u.EscapedPath(), "/") + if hostPath != "" && hostPath != "/" { + if uc.App.SubdirectoryPath() == "" { + uc.App.Subdirectory = hostPath + } else if uc.App.SubdirectoryPath() != hostPath { + log.Info("Config warning: ignoring host path %q because app.subdirectory is set to %q", hostPath, uc.App.SubdirectoryPath()) + } + } + return uc, nil } // Save writes the given Config to the given file. func Save(uc *Config, fname string) error { cfg := ini.Empty() err := ini.ReflectFrom(cfg, uc) if err != nil { return err } if fname == "" { fname = FileName } return cfg.SaveTo(fname) } diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..fa9270a --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,67 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func writeTempConfig(t *testing.T, appSection string) string { + t.Helper() + + dir := t.TempDir() + p := filepath.Join(dir, "config.ini") + contents := "[app]\n" + appSection + "\n" + if err := os.WriteFile(p, []byte(contents), 0o600); err != nil { + t.Fatalf("write config: %v", err) + } + return p +} + +func TestLoadMovesHostPathToSubdirectory(t *testing.T) { + f := writeTempConfig(t, "host = https://example.com/blog") + + cfg, err := Load(f) + if err != nil { + t.Fatalf("load config: %v", err) + } + + if cfg.App.Host != "https://example.com" { + t.Fatalf("expected host without path; got %q", cfg.App.Host) + } + if got := cfg.App.SubdirectoryPath(); got != "/blog" { + t.Fatalf("expected subdirectory /blog; got %q", got) + } +} + +func TestLoadStripsHostPathWhenMatchingSubdirectory(t *testing.T) { + f := writeTempConfig(t, "host = https://example.com/blog\nsubdirectory = /blog") + + cfg, err := Load(f) + if err != nil { + t.Fatalf("load config: %v", err) + } + + if cfg.App.Host != "https://example.com" { + t.Fatalf("expected host without path; got %q", cfg.App.Host) + } + if got := cfg.App.SubdirectoryPath(); got != "/blog" { + t.Fatalf("expected subdirectory /blog; got %q", got) + } +} + +func TestLoadPrefersExplicitSubdirectoryOverHostPath(t *testing.T) { + f := writeTempConfig(t, "host = https://example.com/blog\nsubdirectory = /site") + + cfg, err := Load(f) + if err != nil { + t.Fatalf("load config: %v", err) + } + + if cfg.App.Host != "https://example.com" { + t.Fatalf("expected host without path; got %q", cfg.App.Host) + } + if got := cfg.App.SubdirectoryPath(); got != "/site" { + t.Fatalf("expected explicit subdirectory /site; got %q", got) + } +} diff --git a/config/funcs.go b/config/funcs.go index 3bbcfa7..7c6fd4f 100644 --- a/config/funcs.go +++ b/config/funcs.go @@ -1,63 +1,62 @@ /* * Copyright © 2018, 2020-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package config import ( + "github.com/writeas/web-core/log" + "golang.org/x/net/idna" "net/http" "net/url" "strings" "time" - - "github.com/writeas/web-core/log" - "golang.org/x/net/idna" ) // FriendlyHost returns the app's Host sans any schema func (ac AppCfg) FriendlyHost() string { rawHost := ac.Host[strings.Index(ac.Host, "://")+len("://"):] u, err := url.Parse(ac.Host) if err != nil { log.Error("url.Parse failed on %s: %s", ac.Host, err) return rawHost } d, err := idna.ToUnicode(u.Hostname()) if err != nil { log.Error("idna.ToUnicode failed on %s: %s", ac.Host, err) return rawHost } res := d if u.Port() != "" { res += ":" + u.Port() } return res } func (ac AppCfg) CanCreateBlogs(currentlyUsed uint64) bool { if ac.MaxBlogs <= 0 { return true } return int(currentlyUsed) < ac.MaxBlogs } // OrDefaultString returns input or a default value if input is empty. func OrDefaultString(input, defaultValue string) string { if len(input) == 0 { return defaultValue } return input } // DefaultHTTPClient returns a sane default HTTP client. func DefaultHTTPClient() *http.Client { return &http.Client{Timeout: 10 * time.Second} } diff --git a/config/setup.go b/config/setup.go index 4beef13..c3b9f27 100644 --- a/config/setup.go +++ b/config/setup.go @@ -1,396 +1,407 @@ /* * Copyright © 2018 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package config import ( "fmt" "os" "strconv" "strings" "github.com/fatih/color" "github.com/manifoldco/promptui" "github.com/mitchellh/go-wordwrap" "github.com/writeas/web-core/auth" ) type SetupData struct { User *UserCreation Config *Config } func Configure(fname string, configSections string) (*SetupData, error) { data := &SetupData{} var err error if fname == "" { fname = FileName } data.Config, err = Load(fname) var action string isNewCfg := false if err != nil { fmt.Printf("No %s configuration yet. Creating new.\n", fname) data.Config = New() action = "generate" isNewCfg = true } else { fmt.Printf("Loaded configuration %s.\n", fname) action = "update" } title := color.New(color.Bold, color.BgGreen).PrintFunc() intro := color.New(color.Bold, color.FgWhite).PrintlnFunc() fmt.Println() intro(" ✍ WriteFreely Configuration ✍") fmt.Println() fmt.Println(wordwrap.WrapString(" This quick configuration process will "+action+" the application's config file, "+fname+".\n\n It validates your input along the way, so you can be sure any future errors aren't caused by a bad configuration. If you'd rather configure your server manually, instead run: writefreely --create-config and edit that file.", 75)) fmt.Println() tmpls := &promptui.PromptTemplates{ Success: "{{ . | bold | faint }}: ", } selTmpls := &promptui.SelectTemplates{ Selected: `{{.Label}} {{ . | faint }}`, } var selPrompt promptui.Select var prompt promptui.Prompt if strings.Contains(configSections, "server") { title(" Server setup ") fmt.Println() // Environment selection selPrompt = promptui.Select{ Templates: selTmpls, Label: "Environment", Items: []string{"Development", "Production, standalone", "Production, behind reverse proxy"}, } _, envType, err := selPrompt.Run() if err != nil { return data, err } isDevEnv := envType == "Development" isStandalone := envType == "Production, standalone" _, isDocker := os.LookupEnv("WRITEFREELY_DOCKER") data.Config.Server.Dev = isDevEnv if isDevEnv || !isStandalone { // Running in dev environment or behind reverse proxy; ask for port prompt = promptui.Prompt{ Templates: tmpls, Label: "Local port", Validate: validatePort, Default: fmt.Sprintf("%d", data.Config.Server.Port), } port, err := prompt.Run() if err != nil { return data, err } data.Config.Server.Port, _ = strconv.Atoi(port) // Ignore error, as we've already validated number } if isStandalone { selPrompt = promptui.Select{ Templates: selTmpls, Label: "Web server mode", Items: []string{"Insecure (port 80)", "Secure (port 443), manual certificate", "Secure (port 443), auto certificate"}, } sel, _, err := selPrompt.Run() if err != nil { return data, err } if sel == 0 { data.Config.Server.Autocert = false data.Config.Server.Port = 80 data.Config.Server.TLSCertPath = "" data.Config.Server.TLSKeyPath = "" } else if sel == 1 || sel == 2 { data.Config.Server.Port = 443 data.Config.Server.Autocert = sel == 2 if sel == 1 { // Manual certificate configuration prompt = promptui.Prompt{ Templates: tmpls, Label: "Certificate path", Validate: validateNonEmpty, Default: data.Config.Server.TLSCertPath, } data.Config.Server.TLSCertPath, err = prompt.Run() if err != nil { return data, err } prompt = promptui.Prompt{ Templates: tmpls, Label: "Key path", Validate: validateNonEmpty, Default: data.Config.Server.TLSKeyPath, } data.Config.Server.TLSKeyPath, err = prompt.Run() if err != nil { return data, err } } else { // Automatic certificate data.Config.Server.TLSCertPath = "certs" data.Config.Server.TLSKeyPath = "certs" } } } else { data.Config.Server.TLSCertPath = "" data.Config.Server.TLSKeyPath = "" } // If running in docker: // 1. always bind to 0.0.0.0 instead of localhost // 2. set paths of static files in UNIX manners if !isDevEnv && isDocker { data.Config.Server.TemplatesParentDir = "/usr/share/writefreely" data.Config.Server.StaticParentDir = "/usr/share/writefreely" data.Config.Server.PagesParentDir = "/usr/share/writefreely" data.Config.Server.Bind = "0.0.0.0" } fmt.Println() } if strings.Contains(configSections, "db") { title(" Database setup ") fmt.Println() selPrompt = promptui.Select{ Templates: selTmpls, Label: "Database driver", Items: []string{"MySQL", "SQLite"}, } sel, _, err := selPrompt.Run() if err != nil { return data, err } if sel == 0 { // Configure for MySQL data.Config.UseMySQL(isNewCfg) prompt = promptui.Prompt{ Templates: tmpls, Label: "Username", Validate: validateNonEmpty, Default: data.Config.Database.User, } data.Config.Database.User, err = prompt.Run() if err != nil { return data, err } prompt = promptui.Prompt{ Templates: tmpls, Label: "Password", Validate: validateNonEmpty, Default: data.Config.Database.Password, Mask: '*', } data.Config.Database.Password, err = prompt.Run() if err != nil { return data, err } prompt = promptui.Prompt{ Templates: tmpls, Label: "Database name", Validate: validateNonEmpty, Default: data.Config.Database.Database, } data.Config.Database.Database, err = prompt.Run() if err != nil { return data, err } prompt = promptui.Prompt{ Templates: tmpls, Label: "Host", Validate: validateNonEmpty, Default: data.Config.Database.Host, } data.Config.Database.Host, err = prompt.Run() if err != nil { return data, err } prompt = promptui.Prompt{ Templates: tmpls, Label: "Port", Validate: validatePort, Default: fmt.Sprintf("%d", data.Config.Database.Port), } dbPort, err := prompt.Run() if err != nil { return data, err } data.Config.Database.Port, _ = strconv.Atoi(dbPort) // Ignore error, as we've already validated number } else if sel == 1 { // Configure for SQLite data.Config.UseSQLite(isNewCfg) prompt = promptui.Prompt{ Templates: tmpls, Label: "Filename", Validate: validateNonEmpty, Default: data.Config.Database.FileName, } data.Config.Database.FileName, err = prompt.Run() if err != nil { return data, err } } fmt.Println() } if strings.Contains(configSections, "app") { title(" App setup ") fmt.Println() selPrompt = promptui.Select{ Templates: selTmpls, Label: "Site type", Items: []string{"Single user blog", "Multi-user instance"}, } _, usersType, err := selPrompt.Run() if err != nil { return data, err } data.Config.App.SingleUser = usersType == "Single user blog" if data.Config.App.SingleUser { data.User = &UserCreation{} // prompt for username prompt = promptui.Prompt{ Templates: tmpls, Label: "Admin username", Validate: validateNonEmpty, } data.User.Username, err = prompt.Run() if err != nil { return data, err } // prompt for password prompt = promptui.Prompt{ Templates: tmpls, Label: "Admin password", Validate: validateNonEmpty, } newUserPass, err := prompt.Run() if err != nil { return data, err } data.User.HashedPass, err = auth.HashPass([]byte(newUserPass)) if err != nil { return data, err } } siteNameLabel := "Instance name" if data.Config.App.SingleUser { siteNameLabel = "Blog name" } prompt = promptui.Prompt{ Templates: tmpls, Label: siteNameLabel, Validate: validateNonEmpty, Default: data.Config.App.SiteName, } data.Config.App.SiteName, err = prompt.Run() if err != nil { return data, err } prompt = promptui.Prompt{ Templates: tmpls, Label: "Public URL", Validate: validateDomain, Default: data.Config.App.Host, } data.Config.App.Host, err = prompt.Run() if err != nil { return data, err } + prompt = promptui.Prompt{ + Templates: tmpls, + Label: "Subdirectory (optional, e.g. /blog)", + Validate: validateSubdirectory, + Default: data.Config.App.Subdirectory, + } + data.Config.App.Subdirectory, err = prompt.Run() + if err != nil { + return data, err + } + if !data.Config.App.SingleUser { selPrompt = promptui.Select{ Templates: selTmpls, Label: "Registration", Items: []string{"Open", "Closed"}, } _, regType, err := selPrompt.Run() if err != nil { return data, err } data.Config.App.OpenRegistration = regType == "Open" prompt = promptui.Prompt{ Templates: tmpls, Label: "Max blogs per user", Default: fmt.Sprintf("%d", data.Config.App.MaxBlogs), } maxBlogs, err := prompt.Run() if err != nil { return data, err } data.Config.App.MaxBlogs, _ = strconv.Atoi(maxBlogs) // Ignore error, as we've already validated number } selPrompt = promptui.Select{ Templates: selTmpls, Label: "Federation", Items: []string{"Enabled", "Disabled"}, } _, fedType, err := selPrompt.Run() if err != nil { return data, err } data.Config.App.Federation = fedType == "Enabled" if data.Config.App.Federation { selPrompt = promptui.Select{ Templates: selTmpls, Label: "Usage stats (active users, posts)", Items: []string{"Public", "Private"}, } _, fedStatsType, err := selPrompt.Run() if err != nil { return data, err } data.Config.App.PublicStats = fedStatsType == "Public" selPrompt = promptui.Select{ Templates: selTmpls, Label: "Instance metadata privacy", Items: []string{"Public", "Private"}, } _, fedStatsType, err = selPrompt.Run() if err != nil { return data, err } data.Config.App.Private = fedStatsType == "Private" } } return data, Save(data.Config, fname) } diff --git a/config/validation.go b/config/validation.go index f6b8a14..55e40ca 100644 --- a/config/validation.go +++ b/config/validation.go @@ -1,51 +1,67 @@ /* * Copyright © 2018 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package config import ( "fmt" "regexp" "strconv" ) var ( domainReg = regexp.MustCompile("^https?://") ) const ( minPort = 80 maxPort = 1<<16 - 1 ) func validateDomain(i string) error { if !domainReg.MatchString(i) { return fmt.Errorf("Domain must start with http:// or https://") } return nil } func validatePort(i string) error { p, err := strconv.Atoi(i) if err != nil { return err } if p < minPort || p > maxPort { return fmt.Errorf("Port must be a number %d - %d", minPort, maxPort) } return nil } func validateNonEmpty(i string) error { if i == "" { return fmt.Errorf("Must not be empty") } return nil } + +func validateSubdirectory(i string) error { + if i == "" { + return nil + } + if i == "/" { + return nil + } + if regexp.MustCompile(`://`).MatchString(i) { + return fmt.Errorf("Must be a path like /blog, not a full URL") + } + if regexp.MustCompile(`[?#\s]`).MatchString(i) { + return fmt.Errorf("Must not include query, fragment, or spaces") + } + return nil +} diff --git a/database-sqlite.go b/database-sqlite.go index c34bca2..7d33315 100644 --- a/database-sqlite.go +++ b/database-sqlite.go @@ -1,74 +1,73 @@ //go:build sqlite && !wflib // +build sqlite,!wflib /* * Copyright © 2019-2020 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "database/sql" - "regexp" - "github.com/go-sql-driver/mysql" "github.com/mattn/go-sqlite3" "github.com/writeas/web-core/log" + "regexp" ) func init() { SQLiteEnabled = true regex := func(re, s string) (bool, error) { return regexp.MatchString(re, s) } sql.Register("sqlite3_with_regex", &sqlite3.SQLiteDriver{ ConnectHook: func(conn *sqlite3.SQLiteConn) error { return conn.RegisterFunc("regexp", regex, true) }, }) } func (db *datastore) isDuplicateKeyErr(err error) bool { if db.driverName == driverSQLite { if err, ok := err.(sqlite3.Error); ok { return err.Code == sqlite3.ErrConstraint } } else if db.driverName == driverMySQL { if mysqlErr, ok := err.(*mysql.MySQLError); ok { return mysqlErr.Number == mySQLErrDuplicateKey } } else { log.Error("isDuplicateKeyErr: failed check for unrecognized driver '%s'", db.driverName) } return false } func (db *datastore) isIgnorableError(err error) bool { if db.driverName == driverMySQL { if mysqlErr, ok := err.(*mysql.MySQLError); ok { return mysqlErr.Number == mySQLErrCollationMix } } else { log.Error("isIgnorableError: failed check for unrecognized driver '%s'", db.driverName) } return false } func (db *datastore) isHighLoadError(err error) bool { if db.driverName == driverMySQL { if mysqlErr, ok := err.(*mysql.MySQLError); ok { return mysqlErr.Number == mySQLErrMaxUserConns || mysqlErr.Number == mySQLErrTooManyConns } } return false } diff --git a/database.go b/database.go index 67e4fbb..cf90e5c 100644 --- a/database.go +++ b/database.go @@ -1,3365 +1,3362 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "context" "database/sql" "fmt" "net/http" "net/url" "strings" "time" "github.com/writeas/monday" "github.com/go-sql-driver/mysql" "github.com/writeas/web-core/silobridge" wf_db "github.com/writefreely/writefreely/db" "github.com/writefreely/writefreely/parse" "github.com/guregu/null" "github.com/guregu/null/zero" uuid "github.com/nu7hatch/gouuid" "github.com/writeas/activityserve" "github.com/writeas/impart" "github.com/writeas/web-core/activitypub" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/data" "github.com/writeas/web-core/id" "github.com/writeas/web-core/log" "github.com/writeas/web-core/query" "github.com/writefreely/writefreely/author" "github.com/writefreely/writefreely/config" "github.com/writefreely/writefreely/key" ) const ( mySQLErrDuplicateKey = 1062 mySQLErrCollationMix = 1267 mySQLErrTooManyConns = 1040 mySQLErrMaxUserConns = 1203 driverMySQL = "mysql" driverSQLite = "sqlite3" ) var ( SQLiteEnabled bool ) type writestore interface { CreateUser(*config.Config, *User, string, string) error UpdateUserEmail(keys *key.Keychain, userID int64, email string) error UpdateEncryptedUserEmail(int64, []byte) error GetUserByID(int64) (*User, error) GetUserForAuth(string) (*User, error) GetUserForAuthByID(int64) (*User, error) GetUserNameFromToken(string) (string, error) GetUserDataFromToken(string) (int64, string, error) GetAPIUser(header string) (*User, error) GetUserID(accessToken string) int64 GetUserIDPrivilege(accessToken string) (userID int64, sudo bool) DeleteToken(accessToken []byte) error FetchLastAccessToken(userID int64) string GetAccessToken(userID int64) (string, error) GetTemporaryAccessToken(userID int64, validSecs int) (string, error) GetTemporaryOneTimeAccessToken(userID int64, validSecs int, oneTime bool) (string, error) DeleteAccount(userID int64) error ChangeSettings(app *App, u *User, s *userSettings) error ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error GetCollections(u *User, hostName string) (*[]Collection, error) GetPublishableCollections(u *User, hostName string) (*[]Collection, error) GetMeStats(u *User) userMeStats GetTotalCollections() (int64, error) GetTotalPosts() (int64, error) GetTopPosts(u *User, alias string, hostName string) (*[]PublicPost, error) GetAnonymousPosts(u *User, page int) (*[]PublicPost, error) GetUserPosts(u *User) (*[]PublicPost, error) CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error) CreatePost(userID, collID int64, post *SubmittedPost) (*Post, error) UpdateOwnedPost(post *AuthenticatedPost, userID int64) error GetEditablePost(id, editToken string) (*PublicPost, error) PostIDExists(id string) bool GetPost(id string, collectionID int64) (*PublicPost, error) GetOwnedPost(id string, ownerID int64) (*PublicPost, error) GetPostProperty(id string, collectionID int64, property string) (interface{}, error) CreateCollectionFromToken(*config.Config, string, string, string) (*Collection, error) CreateCollection(*config.Config, string, string, int64) (*Collection, error) GetCollectionBy(condition string, value interface{}) (*Collection, error) GetCollection(alias string) (*Collection, error) GetCollectionForPad(alias string) (*Collection, error) GetCollectionByID(id int64) (*Collection, error) UpdateCollection(app *App, c *SubmittedCollection, alias string) error DeleteCollection(alias string, userID int64) error UpdatePostPinState(pinned bool, postID string, collID, ownerID, pos int64) error GetLastPinnedPostPos(collID int64) int64 GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[]PublicPost, error) RemoveCollectionRedirect(t *sql.Tx, alias string) error GetCollectionRedirect(alias string) (new string) IsCollectionAttributeOn(id int64, attr string) bool CollectionHasAttribute(id int64, attr string) bool CanCollect(cpr *ClaimPostRequest, userID int64) bool AttemptClaim(p *ClaimPostRequest, query string, params []interface{}, slugIdx int) (sql.Result, error) DispersePosts(userID int64, postIDs []string) (*[]ClaimPostResult, error) ClaimPosts(cfg *config.Config, userID int64, collAlias string, posts *[]ClaimPostRequest) (*[]ClaimPostResult, error) GetPostLikeCounts(postID string) (int64, error) GetPostsCount(c *CollectionObj, includeFuture bool) error GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool, contentType PostType) (*[]PublicPost, error) GetAllPostsTaggedIDs(c *Collection, tag string, includeFuture bool) ([]string, error) GetPostsTagged(cfg *config.Config, c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) GetAPFollowers(c *Collection) (*[]RemoteUser, error) GetAPActorKeys(collectionID int64) ([]byte, []byte) CreateUserInvite(id string, userID int64, maxUses int, expires *time.Time) error GetUserInvites(userID int64) (*[]Invite, error) GetUserInvite(id string) (*Invite, error) GetUsersInvitedCount(id string) int64 CreateInvitedUser(inviteID string, userID int64) error GetDynamicContent(id string) (*instanceContent, error) UpdateDynamicContent(id, title, content, contentType string) error GetAllUsers(page uint) (*[]User, error) GetAllUsersCount() int64 GetUserLastPostTime(id int64) (*time.Time, error) GetCollectionLastPostTime(id int64) (*time.Time, error) GetIDForRemoteUser(context.Context, string, string, string) (int64, error) RecordRemoteUserID(context.Context, int64, string, string, string, string) error ValidateOAuthState(context.Context, string) (string, string, int64, string, error) GenerateOAuthState(context.Context, string, string, int64, string) (string, error) GetOauthAccounts(ctx context.Context, userID int64) ([]oauthAccountInfo, error) RemoveOauth(ctx context.Context, userID int64, provider string, clientID string, remoteUserID string) error DatabaseInitialized() bool } type datastore struct { *sql.DB driverName string useSpencerRegex bool } var _ writestore = &datastore{} func (db *datastore) now() string { if db.driverName == driverSQLite { return "strftime('%Y-%m-%d %H:%M:%S','now')" } return "NOW()" } func (db *datastore) clip(field string, l int) string { if db.driverName == driverSQLite { return fmt.Sprintf("SUBSTR(%s, 0, %d)", field, l) } return fmt.Sprintf("LEFT(%s, %d)", field, l) } func (db *datastore) upsert(indexedCols ...string) string { if db.driverName == driverSQLite { // NOTE: SQLite UPSERT syntax only works in v3.24.0 (2018-06-04) or later // Leaving this for whenever we can upgrade and include it in our binary cc := strings.Join(indexedCols, ", ") return "ON CONFLICT(" + cc + ") DO UPDATE SET" } return "ON DUPLICATE KEY UPDATE" } func (db *datastore) dateAdd(l int, unit string) string { if db.driverName == driverSQLite { return fmt.Sprintf("DATETIME('now', '%d %s')", l, unit) } return fmt.Sprintf("DATE_ADD(NOW(), INTERVAL %d %s)", l, unit) } func (db *datastore) dateSub(l int, unit string) string { if db.driverName == driverSQLite { return fmt.Sprintf("DATETIME('now', '-%d %s')", l, unit) } return fmt.Sprintf("DATE_SUB(NOW(), INTERVAL %d %s)", l, unit) } func (db *datastore) version() (string, error) { var v string var err error if db.driverName == driverSQLite { err = db.QueryRow("SELECT sqlite_version()").Scan(&v) } else { err = db.QueryRow("SELECT version()").Scan(&v) } if err != nil { return "", err } return v, nil } // CreateUser creates a new user in the database from the given User, UPDATING it in the process with the user's ID. func (db *datastore) CreateUser(cfg *config.Config, u *User, collectionTitle string, collectionDesc string) error { if db.PostIDExists(u.Username) { return impart.HTTPError{http.StatusConflict, "Invalid collection name."} } // New users get a `users` and `collections` row. t, err := db.Begin() if err != nil { return err } // 1. Add to `users` table // NOTE: Assumes User's Password is already hashed! res, err := t.Exec("INSERT INTO users (username, password, email) VALUES (?, ?, ?)", u.Username, u.HashedPass, u.Email) if err != nil { t.Rollback() if db.isDuplicateKeyErr(err) { return impart.HTTPError{http.StatusConflict, "Username is already taken."} } log.Error("Rolling back users INSERT: %v\n", err) return err } u.ID, err = res.LastInsertId() if err != nil { t.Rollback() log.Error("Rolling back after LastInsertId: %v\n", err) return err } // 2. Create user's Collection if collectionTitle == "" { collectionTitle = u.Username } res, err = t.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", u.Username, collectionTitle, collectionDesc, defaultVisibility(cfg), u.ID, 0) if err != nil { t.Rollback() if db.isDuplicateKeyErr(err) { return impart.HTTPError{http.StatusConflict, "Username is already taken."} } log.Error("Rolling back collections INSERT: %v\n", err) return err } db.RemoveCollectionRedirect(t, u.Username) err = t.Commit() if err != nil { t.Rollback() log.Error("Rolling back after Commit(): %v\n", err) return err } return nil } // FIXME: We're returning errors inconsistently in this file. Do we use Errorf // for returned value, or impart? func (db *datastore) UpdateUserEmail(keys *key.Keychain, userID int64, email string) error { encEmail, err := data.Encrypt(keys.EmailKey, email) if err != nil { return fmt.Errorf("Couldn't encrypt email %s: %s\n", email, err) } return db.UpdateEncryptedUserEmail(userID, encEmail) } func (db *datastore) UpdateEncryptedUserEmail(userID int64, encEmail []byte) error { _, err := db.Exec("UPDATE users SET email = ? WHERE id = ?", encEmail, userID) if err != nil { return fmt.Errorf("Unable to update user email: %s", err) } return nil } func (db *datastore) CreateCollectionFromToken(cfg *config.Config, alias, title, accessToken string) (*Collection, error) { userID := db.GetUserID(accessToken) if userID == -1 { return nil, ErrBadAccessToken } return db.CreateCollection(cfg, alias, title, userID) } func (db *datastore) GetUserCollectionCount(userID int64) (uint64, error) { var collCount uint64 err := db.QueryRow("SELECT COUNT(*) FROM collections WHERE owner_id = ?", userID).Scan(&collCount) switch { case err == sql.ErrNoRows: return 0, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user from database."} case err != nil: log.Error("Couldn't get collections count for user %d: %v", userID, err) return 0, err } return collCount, nil } func (db *datastore) CreateCollection(cfg *config.Config, alias, title string, userID int64) (*Collection, error) { if db.PostIDExists(alias) { return nil, impart.HTTPError{http.StatusConflict, "Invalid collection name."} } // All good, so create new collection res, err := db.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", alias, title, "", defaultVisibility(cfg), userID, 0) if err != nil { if db.isDuplicateKeyErr(err) { return nil, impart.HTTPError{http.StatusConflict, "Collection already exists."} } log.Error("Couldn't add to collections: %v\n", err) return nil, err } c := &Collection{ Alias: alias, Title: title, OwnerID: userID, PublicOwner: false, Public: defaultVisibility(cfg) == CollPublic, } c.ID, err = res.LastInsertId() if err != nil { log.Error("Couldn't get collection LastInsertId: %v\n", err) } return c, nil } func (db *datastore) GetUserByID(id int64) (*User, error) { u := &User{ID: id} err := db.QueryRow("SELECT username, password, email, created, status FROM users WHERE id = ?", id).Scan(&u.Username, &u.HashedPass, &u.Email, &u.Created, &u.Status) switch { case err == sql.ErrNoRows: return nil, ErrUserNotFound case err != nil: log.Error("Couldn't SELECT user password: %v", err) return nil, err } return u, nil } // IsUserSilenced returns true if the user account associated with id is // currently silenced. func (db *datastore) IsUserSilenced(id int64) (bool, error) { u := &User{ID: id} err := db.QueryRow("SELECT status FROM users WHERE id = ?", id).Scan(&u.Status) switch { case err == sql.ErrNoRows: return false, ErrUserNotFound case err != nil: log.Error("Couldn't SELECT user status: %v", err) return false, fmt.Errorf("is user silenced: %v", err) } return u.IsSilenced(), nil } // DoesUserNeedAuth returns true if the user hasn't provided any methods for // authenticating with the account, such a passphrase or email address. // Any errors are reported to admin and silently quashed, returning false as the // result. func (db *datastore) DoesUserNeedAuth(id int64) bool { var pass, email []byte // Find out if user has an email set first err := db.QueryRow("SELECT password, email FROM users WHERE id = ?", id).Scan(&pass, &email) switch { case err == sql.ErrNoRows: // ERROR. Don't give false positives on needing auth methods return false case err != nil: // ERROR. Don't give false positives on needing auth methods log.Error("Couldn't SELECT user %d from users: %v", id, err) return false } // User doesn't need auth if there's an email return len(email) == 0 && len(pass) == 0 } func (db *datastore) IsUserPassSet(id int64) (bool, error) { var pass []byte err := db.QueryRow("SELECT password FROM users WHERE id = ?", id).Scan(&pass) switch { case err == sql.ErrNoRows: return false, nil case err != nil: log.Error("Couldn't SELECT user %d from users: %v", id, err) return false, err } return len(pass) > 0, nil } func (db *datastore) GetUserForAuth(username string) (*User, error) { u := &User{Username: username} err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE username = ?", username).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status) switch { case err == sql.ErrNoRows: // Check if they've entered the wrong, unnormalized username username = getSlug(username, "") if username != u.Username { err = db.QueryRow("SELECT id FROM users WHERE username = ? LIMIT 1", username).Scan(&u.ID) if err == nil { return db.GetUserForAuth(username) } } return nil, ErrUserNotFound case err != nil: log.Error("Couldn't SELECT user password: %v", err) return nil, err } return u, nil } func (db *datastore) GetUserForAuthByID(userID int64) (*User, error) { u := &User{ID: userID} err := db.QueryRow("SELECT id, password, email, created, status FROM users WHERE id = ?", u.ID).Scan(&u.ID, &u.HashedPass, &u.Email, &u.Created, &u.Status) switch { case err == sql.ErrNoRows: return nil, ErrUserNotFound case err != nil: log.Error("Couldn't SELECT userForAuthByID: %v", err) return nil, err } return u, nil } func (db *datastore) GetUserNameFromToken(accessToken string) (string, error) { t := auth.GetToken(accessToken) if len(t) == 0 { return "", ErrNoAccessToken } var oneTime bool var username string err := db.QueryRow("SELECT username, one_time FROM accesstokens LEFT JOIN users ON user_id = id WHERE token LIKE ? AND (expires IS NULL OR expires > "+db.now()+")", t).Scan(&username, &oneTime) switch { case err == sql.ErrNoRows: return "", ErrBadAccessToken case err != nil: return "", ErrInternalGeneral } // Delete token if it was one-time if oneTime { db.DeleteToken(t[:]) } return username, nil } func (db *datastore) GetUserDataFromToken(accessToken string) (int64, string, error) { t := auth.GetToken(accessToken) if len(t) == 0 { return 0, "", ErrNoAccessToken } var userID int64 var oneTime bool var username string err := db.QueryRow("SELECT user_id, username, one_time FROM accesstokens LEFT JOIN users ON user_id = id WHERE token LIKE ? AND (expires IS NULL OR expires > "+db.now()+")", t).Scan(&userID, &username, &oneTime) switch { case err == sql.ErrNoRows: return 0, "", ErrBadAccessToken case err != nil: return 0, "", ErrInternalGeneral } // Delete token if it was one-time if oneTime { db.DeleteToken(t[:]) } return userID, username, nil } func (db *datastore) GetAPIUser(header string) (*User, error) { uID := db.GetUserID(header) if uID == -1 { return nil, fmt.Errorf(ErrUserNotFound.Error()) } return db.GetUserByID(uID) } // GetUserID takes a hexadecimal accessToken, parses it into its binary // representation, and gets any user ID associated with the token. If no user // is associated, -1 is returned. func (db *datastore) GetUserID(accessToken string) int64 { i, _ := db.GetUserIDPrivilege(accessToken) return i } func (db *datastore) GetUserIDPrivilege(accessToken string) (userID int64, sudo bool) { t := auth.GetToken(accessToken) if len(t) == 0 { return -1, false } var oneTime bool err := db.QueryRow("SELECT user_id, sudo, one_time FROM accesstokens WHERE token LIKE ? AND (expires IS NULL OR expires > "+db.now()+")", t).Scan(&userID, &sudo, &oneTime) switch { case err == sql.ErrNoRows: return -1, false case err != nil: return -1, false } // Delete token if it was one-time if oneTime { db.DeleteToken(t[:]) } return } func (db *datastore) DeleteToken(accessToken []byte) error { res, err := db.Exec("DELETE FROM accesstokens WHERE token LIKE ?", accessToken) if err != nil { return err } rowsAffected, _ := res.RowsAffected() if rowsAffected == 0 { return impart.HTTPError{http.StatusNotFound, "Token is invalid or doesn't exist"} } return nil } // FetchLastAccessToken creates a new non-expiring, valid access token for the given // userID. func (db *datastore) FetchLastAccessToken(userID int64) string { var t []byte err := db.QueryRow("SELECT token FROM accesstokens WHERE user_id = ? AND (expires IS NULL OR expires > "+db.now()+") ORDER BY created DESC LIMIT 1", userID).Scan(&t) switch { case err == sql.ErrNoRows: return "" case err != nil: log.Error("Failed selecting from accesstoken: %v", err) return "" } u, err := uuid.Parse(t) if err != nil { return "" } return u.String() } // GetAccessToken creates a new non-expiring, valid access token for the given // userID. func (db *datastore) GetAccessToken(userID int64) (string, error) { return db.GetTemporaryOneTimeAccessToken(userID, 0, false) } // GetTemporaryAccessToken creates a new valid access token for the given // userID that remains valid for the given time in seconds. If validSecs is 0, // the access token doesn't automatically expire. func (db *datastore) GetTemporaryAccessToken(userID int64, validSecs int) (string, error) { return db.GetTemporaryOneTimeAccessToken(userID, validSecs, false) } // GetTemporaryOneTimeAccessToken creates a new valid access token for the given // userID that remains valid for the given time in seconds and can only be used // once if oneTime is true. If validSecs is 0, the access token doesn't // automatically expire. func (db *datastore) GetTemporaryOneTimeAccessToken(userID int64, validSecs int, oneTime bool) (string, error) { u, err := uuid.NewV4() if err != nil { log.Error("Unable to generate token: %v", err) return "", err } // Insert UUID to `accesstokens` binTok := u[:] expirationVal := "NULL" if validSecs > 0 { expirationVal = db.dateAdd(validSecs, "SECOND") } _, err = db.Exec("INSERT INTO accesstokens (token, user_id, one_time, expires) VALUES (?, ?, ?, "+expirationVal+")", string(binTok), userID, oneTime) if err != nil { log.Error("Couldn't INSERT accesstoken: %v", err) return "", err } return u.String(), nil } func (db *datastore) CreatePasswordResetToken(userID int64) (string, error) { t := id.Generate62RandomString(32) _, err := db.Exec("INSERT INTO password_resets (user_id, token, used, created) VALUES (?, ?, 0, "+db.now()+")", userID, t) if err != nil { log.Error("Couldn't INSERT password_resets: %v", err) return "", err } return t, nil } func (db *datastore) GetUserFromPasswordReset(token string) int64 { var userID int64 err := db.QueryRow("SELECT user_id FROM password_resets WHERE token = ? AND used = 0 AND created > "+db.dateSub(3, "HOUR"), token).Scan(&userID) if err != nil { return 0 } return userID } func (db *datastore) ConsumePasswordResetToken(t string) error { _, err := db.Exec("UPDATE password_resets SET used = 1 WHERE token = ?", t) if err != nil { log.Error("Couldn't UPDATE password_resets: %v", err) return err } return nil } func (db *datastore) CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error) { var userID, collID int64 = -1, -1 var coll *Collection var err error if accessToken != "" { userID = db.GetUserID(accessToken) if userID == -1 { return nil, ErrBadAccessToken } if collAlias != "" { coll, err = db.GetCollection(collAlias) if err != nil { return nil, err } coll.hostName = hostName if coll.OwnerID != userID { return nil, ErrForbiddenCollection } collID = coll.ID } } rp := &PublicPost{} rp.Post, err = db.CreatePost(userID, collID, post) if err != nil { return rp, err } if coll != nil { coll.ForPublic() rp.Collection = &CollectionObj{Collection: *coll} } return rp, nil } func (db *datastore) CreatePost(userID, collID int64, post *SubmittedPost) (*Post, error) { idLen := postIDLen friendlyID := id.GenerateFriendlyRandomString(idLen) // Handle appearance / font face appearance := post.Font if !post.isFontValid() { appearance = "norm" } var err error ownerID := sql.NullInt64{ Valid: false, } ownerCollID := sql.NullInt64{ Valid: false, } slug := sql.NullString{"", false} // If an alias was supplied, we'll add this to the collection as well. if userID > 0 { ownerID.Int64 = userID ownerID.Valid = true if collID > 0 { ownerCollID.Int64 = collID ownerCollID.Valid = true var slugVal string if post.Slug != nil && *post.Slug != "" { slugVal = *post.Slug } else { if post.Title != nil && *post.Title != "" { slugVal = getSlug(*post.Title, post.Language.String) if slugVal == "" { slugVal = getSlug(*post.Content, post.Language.String) } } else { slugVal = getSlug(*post.Content, post.Language.String) } } if slugVal == "" { slugVal = friendlyID } slug = sql.NullString{slugVal, true} } } created := time.Now() if db.driverName == driverSQLite { // SQLite stores datetimes in UTC, so convert time.Now() to it here created = created.UTC() } if post.Created != nil && *post.Created != "" { created, err = time.Parse("2006-01-02T15:04:05Z", *post.Created) if err != nil { log.Error("Unable to parse Created time '%s': %v", *post.Created, err) created = time.Now() if db.driverName == driverSQLite { // SQLite stores datetimes in UTC, so convert time.Now() to it here created = created.UTC() } } } stmt, err := db.Prepare("INSERT INTO posts (id, slug, title, content, text_appearance, language, rtl, privacy, owner_id, collection_id, created, updated, view_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " + db.now() + ", ?)") if err != nil { return nil, err } defer stmt.Close() _, err = stmt.Exec(friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, 0, ownerID, ownerCollID, created, 0) if err != nil { if db.isDuplicateKeyErr(err) { // Duplicate entry error; try a new slug // TODO: make this a little more robust slug = sql.NullString{id.GenSafeUniqueSlug(slug.String), true} _, err = stmt.Exec(friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, 0, ownerID, ownerCollID, created, 0) if err != nil { return nil, handleFailedPostInsert(fmt.Errorf("Retried slug generation, still failed: %v", err)) } } else { return nil, handleFailedPostInsert(err) } } // TODO: return Created field in proper format return &Post{ ID: friendlyID, Slug: null.NewString(slug.String, slug.Valid), Font: appearance, Language: zero.NewString(post.Language.String, post.Language.Valid), RTL: zero.NewBool(post.IsRTL.Bool, post.IsRTL.Valid), OwnerID: null.NewInt(userID, true), CollectionID: null.NewInt(userID, true), Created: created.Truncate(time.Second).UTC(), Updated: time.Now().Truncate(time.Second).UTC(), Title: zero.NewString(*(post.Title), true), Content: *(post.Content), }, nil } // UpdateOwnedPost updates an existing post with only the given fields in the // supplied AuthenticatedPost. func (db *datastore) UpdateOwnedPost(post *AuthenticatedPost, userID int64) error { params := []interface{}{} var queryUpdates, sep, authCondition string if post.Slug != nil && *post.Slug != "" { queryUpdates += sep + "slug = ?" sep = ", " params = append(params, getSlug(*post.Slug, "")) } if post.Content != nil { queryUpdates += sep + "content = ?" sep = ", " params = append(params, post.Content) } if post.Title != nil { queryUpdates += sep + "title = ?" sep = ", " params = append(params, post.Title) } if post.Language.Valid { queryUpdates += sep + "language = ?" sep = ", " params = append(params, post.Language.String) } if post.IsRTL.Valid { queryUpdates += sep + "rtl = ?" sep = ", " params = append(params, post.IsRTL.Bool) } if post.Font != "" { queryUpdates += sep + "text_appearance = ?" sep = ", " params = append(params, post.Font) } if post.Created != nil { createTime, err := time.Parse(postMetaDateFormat, *post.Created) if err != nil { log.Error("Unable to parse Created date: %v", err) return fmt.Errorf("That's the incorrect format for Created date.") } queryUpdates += sep + "created = ?" sep = ", " params = append(params, createTime) } // WHERE parameters... // id = ? params = append(params, post.ID) // AND owner_id = ? authCondition = "(owner_id = ?)" params = append(params, userID) if queryUpdates == "" { return ErrPostNoUpdatableVals } queryUpdates += sep + "updated = " + db.now() res, err := db.Exec("UPDATE posts SET "+queryUpdates+" WHERE id = ? AND "+authCondition, params...) if err != nil { log.Error("Unable to update owned post: %v", err) return err } rowsAffected, _ := res.RowsAffected() if rowsAffected == 0 { // Show the correct error message if nothing was updated var dummy int err := db.QueryRow("SELECT 1 FROM posts WHERE id = ? AND "+authCondition, post.ID, params[len(params)-1]).Scan(&dummy) switch { case err == sql.ErrNoRows: return ErrUnauthorizedEditPost case err != nil: log.Error("Failed selecting from posts: %v", err) } return nil } return nil } func (db *datastore) GetCollectionBy(condition string, value interface{}) (*Collection, error) { c := &Collection{} // FIXME: change Collection to reflect database values. Add helper functions to get actual values var styleSheet, script, signature, format zero.String row := db.QueryRow("SELECT id, alias, title, description, style_sheet, script, post_signature, format, owner_id, privacy, view_count FROM collections WHERE "+condition, value) err := row.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &styleSheet, &script, &signature, &format, &c.OwnerID, &c.Visibility, &c.Views) switch { case err == sql.ErrNoRows: return nil, impart.HTTPError{http.StatusNotFound, "Collection doesn't exist."} case db.isHighLoadError(err): return nil, ErrUnavailable case err != nil: log.Error("Failed selecting from collections: %v", err) return nil, err } c.StyleSheet = styleSheet.String c.Script = script.String c.Signature = signature.String c.Format = format.String c.Public = c.IsPublic() c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer") c.Verification = db.GetCollectionAttribute(c.ID, "verification_link") + c.hostName = canonicalAppHost c.db = db return c, nil } func (db *datastore) GetCollection(alias string) (*Collection, error) { return db.GetCollectionBy("alias = ?", alias) } func (db *datastore) GetCollectionForPad(alias string) (*Collection, error) { c := &Collection{Alias: alias} row := db.QueryRow("SELECT id, alias, title, description, privacy FROM collections WHERE alias = ?", alias) err := row.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &c.Visibility) switch { case err == sql.ErrNoRows: return c, impart.HTTPError{http.StatusNotFound, "Collection doesn't exist."} case err != nil: log.Error("Failed selecting from collections: %v", err) return c, ErrInternalGeneral } c.Public = c.IsPublic() + c.hostName = canonicalAppHost return c, nil } func (db *datastore) GetCollectionByID(id int64) (*Collection, error) { return db.GetCollectionBy("id = ?", id) } func (db *datastore) GetCollectionFromDomain(host string) (*Collection, error) { return db.GetCollectionBy("host = ?", host) } func (db *datastore) UpdateCollection(app *App, c *SubmittedCollection, alias string) error { // Truncate fields correctly, so we don't get "Data too long for column" errors in MySQL (writefreely#600) if c.Title != nil { *c.Title = parse.Truncate(*c.Title, collMaxLengthTitle) } if c.Description != nil { *c.Description = parse.Truncate(*c.Description, collMaxLengthDescription) } q := query.NewUpdate(). SetStringPtr(c.Title, "title"). SetStringPtr(c.Description, "description"). SetStringPtr(c.StyleSheet, "style_sheet"). SetStringPtr(c.Script, "script"). SetStringPtr(c.Signature, "post_signature") if c.Format != nil { cf := &CollectionFormat{Format: c.Format.String} if cf.Valid() { q.SetNullString(c.Format, "format") } } var updatePass bool if c.Visibility != nil && (collVisibility(*c.Visibility)&CollProtected == 0 || c.Pass != "") { q.SetIntPtr(c.Visibility, "privacy") if c.Pass != "" { updatePass = true } } // WHERE values q.Where("alias = ? AND owner_id = ?", alias, c.OwnerID) if q.Updates == "" && c.Monetization == nil { return ErrPostNoUpdatableVals } // Find any current domain var collID int64 var rowsAffected int64 var changed bool var res sql.Result err := db.QueryRow("SELECT id FROM collections WHERE alias = ?", alias).Scan(&collID) if err != nil { log.Error("Failed selecting from collections: %v. Some things won't work.", err) } // Update MathJax value if c.MathJax { if db.driverName == driverSQLite { _, err = db.Exec("INSERT OR REPLACE INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?)", collID, "render_mathjax", "1") } else { _, err = db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) "+db.upsert("collection_id", "attribute")+" value = ?", collID, "render_mathjax", "1", "1") } if err != nil { log.Error("Unable to insert render_mathjax value: %v", err) return err } } else { _, err = db.Exec("DELETE FROM collectionattributes WHERE collection_id = ? AND attribute = ?", collID, "render_mathjax") if err != nil { log.Error("Unable to delete render_mathjax value: %v", err) return err } } // Update Verification link value if c.Verification != nil { skipUpdate := false if *c.Verification != "" { // Strip away any excess spaces trimmed := strings.TrimSpace(*c.Verification) if strings.HasPrefix(trimmed, "@") && strings.Count(trimmed, "@") == 2 { // This looks like a fediverse handle, so resolve profile URL profileURL, err := GetProfileURLFromHandle(app, trimmed) if err != nil || profileURL == "" { log.Error("Couldn't find user %s: %v", trimmed, err) skipUpdate = true } else { c.Verification = &profileURL } } else { if !strings.HasPrefix(trimmed, "http") { trimmed = "https://" + trimmed } vu, err := url.Parse(trimmed) if err != nil { // Value appears invalid, so don't update skipUpdate = true } else { s := vu.String() c.Verification = &s } } } if !skipUpdate { err = db.SetCollectionAttribute(collID, "verification_link", *c.Verification) if err != nil { log.Error("Unable to insert verification_link value: %v", err) return err } } } // Update Monetization value if c.Monetization != nil { skipUpdate := false if *c.Monetization != "" { // Strip away any excess spaces trimmed := strings.TrimSpace(*c.Monetization) // Only update value when it starts with "$", per spec: https://paymentpointers.org if strings.HasPrefix(trimmed, "$") { c.Monetization = &trimmed } else { // Value appears invalid, so don't update skipUpdate = true } } if !skipUpdate { _, err = db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) "+db.upsert("collection_id", "attribute")+" value = ?", collID, "monetization_pointer", *c.Monetization, *c.Monetization) if err != nil { log.Error("Unable to insert monetization_pointer value: %v", err) return err } } } // Update EmailSub value if c.EmailSubs { err = db.SetCollectionAttribute(collID, "email_subs", "1") if err != nil { log.Error("Unable to insert email_subs value: %v", err) return err } skipUpdate := false if c.LetterReply != nil { // Strip away any excess spaces trimmed := strings.TrimSpace(*c.LetterReply) // Only update value when it contains "@" if strings.IndexRune(trimmed, '@') > 0 { c.LetterReply = &trimmed } else { // Value appears invalid, so don't update skipUpdate = true } if !skipUpdate { err = db.SetCollectionAttribute(collID, collAttrLetterReplyTo, *c.LetterReply) if err != nil { log.Error("Unable to insert %s value: %v", collAttrLetterReplyTo, err) return err } } } } else { _, err = db.Exec("DELETE FROM collectionattributes WHERE collection_id = ? AND attribute = ?", collID, "email_subs") if err != nil { log.Error("Unable to delete email_subs value: %v", err) return err } } // Update rest of the collection data if q.Updates != "" { res, err = db.Exec("UPDATE collections SET "+q.Updates+" WHERE "+q.Conditions, q.Params...) if err != nil { log.Error("Unable to update collection: %v", err) return err } } rowsAffected, _ = res.RowsAffected() if !changed || rowsAffected == 0 { // Show the correct error message if nothing was updated var dummy int err := db.QueryRow("SELECT 1 FROM collections WHERE alias = ? AND owner_id = ?", alias, c.OwnerID).Scan(&dummy) switch { case err == sql.ErrNoRows: return ErrUnauthorizedEditPost case err != nil: log.Error("Failed selecting from collections: %v", err) } if !updatePass { return nil } } if updatePass { hashedPass, err := auth.HashPass([]byte(c.Pass)) if err != nil { log.Error("Unable to create hash: %s", err) return impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."} } if db.driverName == driverSQLite { _, err = db.Exec("INSERT OR REPLACE INTO collectionpasswords (collection_id, password) VALUES ((SELECT id FROM collections WHERE alias = ?), ?)", alias, hashedPass) } else { _, err = db.Exec("INSERT INTO collectionpasswords (collection_id, password) VALUES ((SELECT id FROM collections WHERE alias = ?), ?) "+db.upsert("collection_id")+" password = ?", alias, hashedPass, hashedPass) } if err != nil { return err } } return nil } const postCols = "id, slug, text_appearance, language, rtl, privacy, owner_id, collection_id, pinned_position, created, updated, view_count, title, content" // getEditablePost returns a PublicPost with the given ID only if the given // edit token is valid for the post. func (db *datastore) GetEditablePost(id, editToken string) (*PublicPost, error) { // FIXME: code duplicated from getPost() // TODO: add slight logic difference to getPost / one func var ownerName sql.NullString p := &Post{} row := db.QueryRow("SELECT "+postCols+", (SELECT username FROM users WHERE users.id = posts.owner_id) AS username FROM posts WHERE id = ? LIMIT 1", id) err := row.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content, &ownerName) switch { case err == sql.ErrNoRows: return nil, ErrPostNotFound case err != nil: log.Error("Failed selecting from collections: %v", err) return nil, err } if p.Content == "" && p.Title.String == "" { return nil, ErrPostUnpublished } res := p.processPost() if ownerName.Valid { res.Owner = &PublicUser{Username: ownerName.String} } return &res, nil } func (db *datastore) PostIDExists(id string) bool { var dummy bool err := db.QueryRow("SELECT 1 FROM posts WHERE id = ?", id).Scan(&dummy) return err == nil && dummy } // GetPost gets a public-facing post object from the database. If collectionID // is > 0, the post will be retrieved by slug and collection ID, rather than // post ID. // TODO: break this into two functions: // - GetPost(id string) // - GetCollectionPost(slug string, collectionID int64) func (db *datastore) GetPost(id string, collectionID int64) (*PublicPost, error) { var ownerName sql.NullString p := &Post{} var row *sql.Row var where string params := []interface{}{id} if collectionID > 0 { where = "slug = ? AND collection_id = ?" params = append(params, collectionID) } else { where = "id = ?" } row = db.QueryRow("SELECT "+postCols+", (SELECT username FROM users WHERE users.id = posts.owner_id) AS username FROM posts WHERE "+where+" LIMIT 1", params...) err := row.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content, &ownerName) switch { case err == sql.ErrNoRows: if collectionID > 0 { return nil, ErrCollectionPageNotFound } return nil, ErrPostNotFound case err != nil: log.Error("Failed selecting from collections: %v", err) return nil, err } if p.Content == "" && p.Title.String == "" { return nil, ErrPostUnpublished } // Get additional information needed before processing post data p.LikeCount, err = db.GetPostLikeCounts(p.ID) if err != nil { return nil, err } res := p.processPost() if ownerName.Valid { res.Owner = &PublicUser{Username: ownerName.String} } return &res, nil } // TODO: don't duplicate getPost() functionality func (db *datastore) GetOwnedPost(id string, ownerID int64) (*PublicPost, error) { p := &Post{} var row *sql.Row where := "id = ? AND owner_id = ?" params := []interface{}{id, ownerID} row = db.QueryRow("SELECT "+postCols+" FROM posts WHERE "+where+" LIMIT 1", params...) err := row.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content) switch { case err == sql.ErrNoRows: return nil, ErrPostNotFound case err != nil: log.Error("Failed selecting from collections: %v", err) return nil, err } if p.Content == "" && p.Title.String == "" { return nil, ErrPostUnpublished } res := p.processPost() return &res, nil } func (db *datastore) GetPostProperty(id string, collectionID int64, property string) (interface{}, error) { propSelects := map[string]string{ "views": "view_count AS views", } selectQuery, ok := propSelects[property] if !ok { return nil, impart.HTTPError{http.StatusBadRequest, fmt.Sprintf("Invalid property: %s.", property)} } var res interface{} var row *sql.Row if collectionID != 0 { row = db.QueryRow("SELECT "+selectQuery+" FROM posts WHERE slug = ? AND collection_id = ? LIMIT 1", id, collectionID) } else { row = db.QueryRow("SELECT "+selectQuery+" FROM posts WHERE id = ? LIMIT 1", id) } err := row.Scan(&res) switch { case err == sql.ErrNoRows: return nil, impart.HTTPError{http.StatusNotFound, "Post not found."} case err != nil: log.Error("Failed selecting post: %v", err) return nil, err } return res, nil } func (db *datastore) GetPostLikeCounts(postID string) (int64, error) { var count int64 err := db.QueryRow("SELECT COUNT(*) FROM remote_likes WHERE post_id = ?", postID).Scan(&count) switch { case err == sql.ErrNoRows: count = 0 case err != nil: return 0, err } return count, nil } // GetPostsCount modifies the CollectionObj to include the correct number of // standard (non-pinned) posts. It will return future posts if `includeFuture` // is true. func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) error { var count int64 timeCondition := "" if !includeFuture { timeCondition = "AND created <= " + db.now() } err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE collection_id = ? AND pinned_position IS NULL "+timeCondition, c.ID).Scan(&count) switch { case err == sql.ErrNoRows: c.TotalPosts = 0 case err != nil: log.Error("Failed selecting from collections: %v", err) c.TotalPosts = 0 return err } c.TotalPosts = int(count) return nil } // GetPosts retrieves all posts for the given Collection. // It will return future posts if `includeFuture` is true. // It will include only standard (non-pinned) posts unless `includePinned` is true. // TODO: change includeFuture to isOwner, since that's how it's used func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool, contentType PostType) (*[]PublicPost, error) { collID := c.ID cf := c.NewFormat() order := "DESC" if cf.Ascending() && !forceRecentFirst { order = "ASC" } pagePosts := cf.PostsPerPage() start := page*pagePosts - pagePosts if page == 0 { start = 0 pagePosts = 1000 } else if contentType == postArch { pagePosts = postsPerArchPage start = page*pagePosts - pagePosts } limitStr := "" if page > 0 { limitStr = fmt.Sprintf(" LIMIT %d, %d", start, pagePosts) } timeCondition := "" if !includeFuture { timeCondition = "AND created <= " + db.now() } pinnedCondition := "" if !includePinned { pinnedCondition = "AND pinned_position IS NULL" } // FUTURE: handle different post contentType's here rows, err := db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? "+pinnedCondition+" "+timeCondition+" ORDER BY created "+order+limitStr, collID) if err != nil { log.Error("Failed selecting from posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve collection posts."} } defer rows.Close() // TODO: extract this common row scanning logic for queries using `postCols` posts := []PublicPost{} for rows.Next() { p := &Post{} err = rows.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content) if err != nil { log.Error("Failed scanning row: %v", err) break } p.extractData() p.augmentContent(c) p.formatContent(cfg, c, includeFuture, false) pubPost := p.processPost() if contentType == postArch { // Overwrite DisplayDate with special Archive page version loc := monday.FuzzyLocale(pubPost.Language.String) pubPost.DisplayDate = monday.Format(pubPost.Created, monday.LongNoYrFormatsByLocale[loc], loc) } posts = append(posts, pubPost) } err = rows.Err() if err != nil { log.Error("Error after Next() on rows: %v", err) } return &posts, nil } func (db *datastore) GetAllPostsTaggedIDs(c *Collection, tag string, includeFuture bool) ([]string, error) { collID := c.ID cf := c.NewFormat() order := "DESC" if cf.Ascending() { order = "ASC" } timeCondition := "" if !includeFuture { timeCondition = "AND created <= " + db.now() } var rows *sql.Rows var err error if db.driverName == driverSQLite { rows, err = db.Query("SELECT id FROM posts WHERE collection_id = ? AND LOWER(content) regexp ? "+timeCondition+" ORDER BY created "+order, collID, `.*#`+strings.ToLower(tag)+`\b.*`) } else { rows, err = db.Query("SELECT id FROM posts WHERE collection_id = ? AND LOWER(content) RLIKE ? "+timeCondition+" ORDER BY created "+order, collID, "#"+strings.ToLower(tag)+"[[:>:]]") } if err != nil { log.Error("Failed selecting tagged posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve tagged collection posts."} } defer rows.Close() ids := []string{} for rows.Next() { var id string err = rows.Scan(&id) if err != nil { log.Error("Failed scanning row: %v", err) break } ids = append(ids, id) } err = rows.Err() if err != nil { log.Error("Error after Next() on rows: %v", err) } return ids, nil } // GetPostsTagged retrieves all posts on the given Collection that contain the // given tag. // It will return future posts if `includeFuture` is true. // TODO: change includeFuture to isOwner, since that's how it's used func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) { collID := c.ID cf := c.NewFormat() order := "DESC" if cf.Ascending() { order = "ASC" } pagePosts := cf.PostsPerPage() start := page*pagePosts - pagePosts if page == 0 { start = 0 pagePosts = 1000 } limitStr := "" if page > 0 { limitStr = fmt.Sprintf(" LIMIT %d, %d", start, pagePosts) } timeCondition := "" if !includeFuture { timeCondition = "AND created <= " + db.now() } var rows *sql.Rows var err error if db.driverName == driverSQLite { rows, err = db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? AND LOWER(content) regexp ? "+timeCondition+" ORDER BY created "+order+limitStr, collID, `.*#`+strings.ToLower(tag)+`\b.*`) } else { var boundaryRegex string if db.useSpencerRegex { // MySQL earlier than 8.0.4, Henry Spencer's regex implementation boundaryRegex = "[[:>:]]" } else { // MySQL 8.0.4+, International Components for Unicode (ICU) syntax boundaryRegex = "\\b" } rows, err = db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? AND LOWER(content) RLIKE ? "+timeCondition+" ORDER BY created "+order+limitStr, collID, "#"+strings.ToLower(tag)+boundaryRegex) } if err != nil { log.Error("Failed selecting from posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve collection posts."} } defer rows.Close() // TODO: extract this common row scanning logic for queries using `postCols` posts := []PublicPost{} for rows.Next() { p := &Post{} err = rows.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content) if err != nil { log.Error("Failed scanning row: %v", err) break } p.extractData() p.augmentContent(c) p.formatContent(cfg, c, includeFuture, false) posts = append(posts, p.processPost()) } err = rows.Err() if err != nil { log.Error("Error after Next() on rows: %v", err) } return &posts, nil } func (db *datastore) GetCollLangTotalPosts(collID int64, lang string) (uint64, error) { var articles uint64 err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE collection_id = ? AND language = ? AND created <= "+db.now(), collID, lang).Scan(&articles) if err != nil && err != sql.ErrNoRows { log.Error("Couldn't get total lang posts count for collection %d: %v", collID, err) return 0, err } return articles, nil } func (db *datastore) GetLangPosts(cfg *config.Config, c *Collection, lang string, page int, includeFuture bool) (*[]PublicPost, error) { collID := c.ID cf := c.NewFormat() order := "DESC" if cf.Ascending() { order = "ASC" } pagePosts := cf.PostsPerPage() start := page*pagePosts - pagePosts if page == 0 { start = 0 pagePosts = 1000 } limitStr := "" if page > 0 { limitStr = fmt.Sprintf(" LIMIT %d, %d", start, pagePosts) } timeCondition := "" if !includeFuture { timeCondition = "AND created <= " + db.now() } rows, err := db.Query(`SELECT `+postCols+` FROM posts WHERE collection_id = ? AND language = ? `+timeCondition+` ORDER BY created `+order+limitStr, collID, lang) if err != nil { log.Error("Failed selecting from posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve collection posts."} } defer rows.Close() // TODO: extract this common row scanning logic for queries using `postCols` posts := []PublicPost{} for rows.Next() { p := &Post{} err = rows.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content) if err != nil { log.Error("Failed scanning row: %v", err) break } p.extractData() p.augmentContent(c) p.formatContent(cfg, c, includeFuture, false) posts = append(posts, p.processPost()) } err = rows.Err() if err != nil { log.Error("Error after Next() on rows: %v", err) } return &posts, nil } func (db *datastore) GetAPFollowers(c *Collection) (*[]RemoteUser, error) { - rows, err := db.Query(`SELECT actor_id, inbox, shared_inbox, f.created -FROM remotefollows f -INNER JOIN remoteusers u - ON f.remote_user_id = u.id -WHERE collection_id = ? -ORDER BY created DESC`, c.ID) + rows, err := db.Query("SELECT actor_id, inbox, shared_inbox, f.created FROM remotefollows f INNER JOIN remoteusers u ON f.remote_user_id = u.id WHERE collection_id = ?", c.ID) if err != nil { log.Error("Failed selecting from followers: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve followers."} } defer rows.Close() followers := []RemoteUser{} for rows.Next() { f := RemoteUser{} err = rows.Scan(&f.ActorID, &f.Inbox, &f.SharedInbox, &f.Created) followers = append(followers, f) } return &followers, nil } // CanCollect returns whether or not the given user can add the given post to a // collection. This is true when a post is already owned by the user. // NOTE: this is currently only used to potentially add owned posts to a // collection. This has the SIDE EFFECT of also generating a slug for the post. // FIXME: make this side effect more explicit (or extract it) func (db *datastore) CanCollect(cpr *ClaimPostRequest, userID int64) bool { var title, content string var lang sql.NullString err := db.QueryRow("SELECT title, content, language FROM posts WHERE id = ? AND owner_id = ?", cpr.ID, userID).Scan(&title, &content, &lang) switch { case err == sql.ErrNoRows: return false case err != nil: log.Error("Failed on post CanCollect(%s, %d): %v", cpr.ID, userID, err) return false } // Since we have the post content and the post is collectable, generate the // post's slug now. cpr.Slug = getSlugFromPost(title, content, lang.String) return true } func (db *datastore) AttemptClaim(p *ClaimPostRequest, query string, params []interface{}, slugIdx int) (sql.Result, error) { qRes, err := db.Exec(query, params...) if err != nil { if db.isDuplicateKeyErr(err) && slugIdx > -1 { s := id.GenSafeUniqueSlug(p.Slug) if s == p.Slug { // Sanity check to prevent infinite recursion return qRes, fmt.Errorf("GenSafeUniqueSlug generated nothing unique: %s", s) } p.Slug = s params[slugIdx] = p.Slug return db.AttemptClaim(p, query, params, slugIdx) } return qRes, fmt.Errorf("attemptClaim: %s", err) } return qRes, nil } func (db *datastore) DispersePosts(userID int64, postIDs []string) (*[]ClaimPostResult, error) { postClaimReqs := map[string]bool{} res := []ClaimPostResult{} for i := range postIDs { postID := postIDs[i] r := ClaimPostResult{Code: 0, ErrorMessage: ""} // Perform post validation if postID == "" { r.ErrorMessage = "Missing post ID. " } if _, ok := postClaimReqs[postID]; ok { r.Code = 429 r.ErrorMessage = "You've already tried anonymizing this post." r.ID = postID res = append(res, r) continue } postClaimReqs[postID] = true var err error // Get full post information to return var fullPost *PublicPost fullPost, err = db.GetPost(postID, 0) if err != nil { if err, ok := err.(impart.HTTPError); ok { r.Code = err.Status r.ErrorMessage = err.Message r.ID = postID res = append(res, r) continue } else { log.Error("Error getting post in dispersePosts: %v", err) } } if fullPost.OwnerID.Int64 != userID { r.Code = http.StatusConflict r.ErrorMessage = "Post is already owned by someone else." r.ID = postID res = append(res, r) continue } var qRes sql.Result var query string var params []interface{} // Do AND owner_id = ? for sanity. // This should've been caught and returned with a good error message // just above. query = "UPDATE posts SET collection_id = NULL WHERE id = ? AND owner_id = ?" params = []interface{}{postID, userID} qRes, err = db.Exec(query, params...) if err != nil { r.Code = http.StatusInternalServerError r.ErrorMessage = "A glitch happened on our end." r.ID = postID res = append(res, r) log.Error("dispersePosts (post %s): %v", postID, err) continue } // Post was successfully dispersed r.Code = http.StatusOK r.Post = fullPost rowsAffected, _ := qRes.RowsAffected() if rowsAffected == 0 { // This was already claimed, but return 200 r.Code = http.StatusOK } res = append(res, r) } return &res, nil } func (db *datastore) ClaimPosts(cfg *config.Config, userID int64, collAlias string, posts *[]ClaimPostRequest) (*[]ClaimPostResult, error) { postClaimReqs := map[string]bool{} res := []ClaimPostResult{} postCollAlias := collAlias for i := range *posts { p := (*posts)[i] if &p == nil { continue } r := ClaimPostResult{Code: 0, ErrorMessage: ""} // Perform post validation if p.ID == "" { r.ErrorMessage = "Missing post ID `id`. " } if _, ok := postClaimReqs[p.ID]; ok { r.Code = 429 r.ErrorMessage = "You've already tried claiming this post." r.ID = p.ID res = append(res, r) continue } postClaimReqs[p.ID] = true canCollect := db.CanCollect(&p, userID) if !canCollect && p.Token == "" { // TODO: ensure post isn't owned by anyone else when a valid modify // token is given. r.ErrorMessage += "Missing post Edit Token `token`." } if r.ErrorMessage != "" { // Post validate failed r.Code = http.StatusBadRequest r.ID = p.ID res = append(res, r) continue } var err error var qRes sql.Result var query string var params []interface{} var slugIdx int = -1 var coll *Collection if collAlias == "" { // Posts are being claimed at /posts/claim, not // /collections/{alias}/collect, so use given individual collection // to associate post with. postCollAlias = p.CollectionAlias } if postCollAlias != "" { // Associate this post with a collection if p.CreateCollection { // This is a new collection // TODO: consider removing this. This seriously complicates this // method and adds another (unnecessary?) logic path. coll, err = db.CreateCollection(cfg, postCollAlias, "", userID) if err != nil { if err, ok := err.(impart.HTTPError); ok { r.Code = err.Status r.ErrorMessage = err.Message } else { r.Code = http.StatusInternalServerError r.ErrorMessage = "Unknown error occurred creating collection" } r.ID = p.ID res = append(res, r) continue } } else { // Attempt to add to existing collection coll, err = db.GetCollection(postCollAlias) if err != nil { if err, ok := err.(impart.HTTPError); ok { if err.Status == http.StatusNotFound { // Show obfuscated "forbidden" response, as if attempting to add to an // unowned blog. r.Code = ErrForbiddenCollection.Status r.ErrorMessage = ErrForbiddenCollection.Message } else { r.Code = err.Status r.ErrorMessage = err.Message } } else { r.Code = http.StatusInternalServerError r.ErrorMessage = "Unknown error occurred claiming post with collection" } r.ID = p.ID res = append(res, r) continue } if coll.OwnerID != userID { r.Code = ErrForbiddenCollection.Status r.ErrorMessage = ErrForbiddenCollection.Message r.ID = p.ID res = append(res, r) continue } } if p.Slug == "" { p.Slug = p.ID } if canCollect { // User already owns this post, so just add it to the given // collection. query = "UPDATE posts SET collection_id = ?, slug = ? WHERE id = ? AND owner_id = ?" params = []interface{}{coll.ID, p.Slug, p.ID, userID} slugIdx = 1 } else { query = "UPDATE posts SET owner_id = ?, collection_id = ?, slug = ? WHERE id = ? AND modify_token = ? AND owner_id IS NULL" params = []interface{}{userID, coll.ID, p.Slug, p.ID, p.Token} slugIdx = 2 } } else { query = "UPDATE posts SET owner_id = ? WHERE id = ? AND modify_token = ? AND owner_id IS NULL" params = []interface{}{userID, p.ID, p.Token} } qRes, err = db.AttemptClaim(&p, query, params, slugIdx) if err != nil { r.Code = http.StatusInternalServerError r.ErrorMessage = "An unknown error occurred." r.ID = p.ID res = append(res, r) log.Error("claimPosts (post %s): %v", p.ID, err) continue } // Get full post information to return var fullPost *PublicPost if p.Token != "" { fullPost, err = db.GetEditablePost(p.ID, p.Token) } else { fullPost, err = db.GetPost(p.ID, 0) } if err != nil { if err, ok := err.(impart.HTTPError); ok { r.Code = err.Status r.ErrorMessage = err.Message r.ID = p.ID res = append(res, r) continue } } if fullPost.OwnerID.Int64 != userID { r.Code = http.StatusConflict r.ErrorMessage = "Post is already owned by someone else." r.ID = p.ID res = append(res, r) continue } // Post was successfully claimed r.Code = http.StatusOK r.Post = fullPost if coll != nil { r.Post.Collection = &CollectionObj{Collection: *coll} } rowsAffected, _ := qRes.RowsAffected() if rowsAffected == 0 { // This was already claimed, but return 200 r.Code = http.StatusOK } res = append(res, r) } return &res, nil } func (db *datastore) UpdatePostPinState(pinned bool, postID string, collID, ownerID, pos int64) error { if pos <= 0 || pos > 20 { pos = db.GetLastPinnedPostPos(collID) + 1 if pos == -1 { pos = 1 } } var err error if pinned { _, err = db.Exec("UPDATE posts SET pinned_position = ? WHERE id = ?", pos, postID) } else { _, err = db.Exec("UPDATE posts SET pinned_position = NULL WHERE id = ?", postID) } if err != nil { log.Error("Unable to update pinned post: %v", err) return err } return nil } func (db *datastore) GetLastPinnedPostPos(collID int64) int64 { var lastPos sql.NullInt64 err := db.QueryRow("SELECT MAX(pinned_position) FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL", collID).Scan(&lastPos) switch { case err == sql.ErrNoRows: return -1 case err != nil: log.Error("Failed selecting from posts: %v", err) return -1 } if !lastPos.Valid { return -1 } return lastPos.Int64 } func (db *datastore) GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[]PublicPost, error) { // FIXME: sqlite-backed instances don't include ellipsis on truncated titles timeCondition := "" if !includeFuture { timeCondition = "AND created <= " + db.now() } rows, err := db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL "+timeCondition+" ORDER BY pinned_position ASC", coll.ID) if err != nil { log.Error("Failed selecting pinned posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve pinned posts."} } defer rows.Close() posts := []PublicPost{} for rows.Next() { p := &Post{} err = rows.Scan(&p.ID, &p.Slug, &p.Title, &p.Content, &p.PinnedPosition) if err != nil { log.Error("Failed scanning row: %v", err) break } p.extractData() p.augmentContent(&coll.Collection) pp := p.processPost() pp.Collection = coll posts = append(posts, pp) } return &posts, nil } func (db *datastore) GetCollections(u *User, hostName string) (*[]Collection, error) { rows, err := db.Query("SELECT id, alias, title, description, privacy, view_count FROM collections WHERE owner_id = ? ORDER BY id ASC", u.ID) if err != nil { log.Error("Failed selecting from collections: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user collections."} } defer rows.Close() colls := []Collection{} for rows.Next() { c := Collection{} err = rows.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &c.Visibility, &c.Views) if err != nil { log.Error("Failed scanning row: %v", err) break } c.hostName = hostName c.URL = c.CanonicalURL() c.Public = c.IsPublic() /* // NOTE: future functionality if visibility != nil { // TODO: && visibility == CollPublic { // Add Monetization info when retrieving all public collections c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer") } */ colls = append(colls, c) } err = rows.Err() if err != nil { log.Error("Error after Next() on rows: %v", err) } return &colls, nil } func (db *datastore) GetPublishableCollections(u *User, hostName string) (*[]Collection, error) { c, err := db.GetCollections(u, hostName) if err != nil { return nil, err } if len(*c) == 0 { return nil, impart.HTTPError{http.StatusInternalServerError, "You don't seem to have any blogs; they might've moved to another account. Try logging out and logging into your other account."} } return c, nil } func (db *datastore) GetPublicCollections(hostName string) (*[]Collection, error) { rows, err := db.Query(`SELECT c.id, alias, title, description, privacy, view_count FROM collections c LEFT JOIN users u ON u.id = c.owner_id WHERE c.privacy = 1 AND u.status = 0 ORDER BY title ASC`) if err != nil { log.Error("Failed selecting public collections: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve public collections."} } defer rows.Close() colls := []Collection{} for rows.Next() { c := Collection{} err = rows.Scan(&c.ID, &c.Alias, &c.Title, &c.Description, &c.Visibility, &c.Views) if err != nil { log.Error("Failed scanning row: %v", err) break } c.hostName = hostName c.URL = c.CanonicalURL() c.Public = c.IsPublic() // Add Monetization information c.Monetization = db.GetCollectionAttribute(c.ID, "monetization_pointer") colls = append(colls, c) } err = rows.Err() if err != nil { log.Error("Error after Next() on rows: %v", err) } return &colls, nil } func (db *datastore) GetMeStats(u *User) userMeStats { s := userMeStats{} // User counts colls, _ := db.GetUserCollectionCount(u.ID) s.TotalCollections = colls var articles, collPosts uint64 err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE owner_id = ? AND collection_id IS NULL", u.ID).Scan(&articles) if err != nil && err != sql.ErrNoRows { log.Error("Couldn't get articles count for user %d: %v", u.ID, err) } s.TotalArticles = articles err = db.QueryRow("SELECT COUNT(*) FROM posts WHERE owner_id = ? AND collection_id IS NOT NULL", u.ID).Scan(&collPosts) if err != nil && err != sql.ErrNoRows { log.Error("Couldn't get coll posts count for user %d: %v", u.ID, err) } s.CollectionPosts = collPosts return s } func (db *datastore) GetTotalCollections() (collCount int64, err error) { err = db.QueryRow(` SELECT COUNT(*) FROM collections c LEFT JOIN users u ON u.id = c.owner_id WHERE u.status = 0`).Scan(&collCount) if err != nil { log.Error("Unable to fetch collections count: %v", err) } return } func (db *datastore) GetTotalPosts() (postCount int64, err error) { err = db.QueryRow(` SELECT COUNT(*) FROM posts p LEFT JOIN users u ON u.id = p.owner_id WHERE u.status = 0`).Scan(&postCount) if err != nil { log.Error("Unable to fetch posts count: %v", err) } return } func (db *datastore) GetTopPosts(u *User, alias string, hostName string) (*[]PublicPost, error) { params := []interface{}{u.ID} where := "" if alias != "" { where = " AND alias = ?" params = append(params, alias) } rows, err := db.Query("SELECT p.id, p.slug, p.view_count, p.title, p.content, c.alias, c.title, c.description, c.view_count FROM posts p LEFT JOIN collections c ON p.collection_id = c.id WHERE p.owner_id = ?"+where+" ORDER BY p.view_count DESC, created DESC LIMIT 25", params...) if err != nil { log.Error("Failed selecting from posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user top posts."} } defer rows.Close() posts := []PublicPost{} var gotErr bool for rows.Next() { p := Post{} c := Collection{} var alias, title, description sql.NullString var views sql.NullInt64 err = rows.Scan(&p.ID, &p.Slug, &p.ViewCount, &p.Title, &p.Content, &alias, &title, &description, &views) if err != nil { log.Error("Failed scanning User.getPosts() row: %v", err) gotErr = true break } p.extractData() pubPost := p.processPost() if alias.Valid && alias.String != "" { c.Alias = alias.String c.Title = title.String c.Description = description.String c.Views = views.Int64 c.hostName = hostName pubPost.Collection = &CollectionObj{Collection: c} } p.LikeCount, err = db.GetPostLikeCounts(p.ID) if err != nil { log.Error("Failed GetPostLikeCounts(%s): %v", p.ID, err) gotErr = true break } posts = append(posts, pubPost) } err = rows.Err() if err != nil { log.Error("Error after Next() on rows: %v", err) } if gotErr && len(posts) == 0 { // There were a lot of errors return nil, impart.HTTPError{http.StatusInternalServerError, "Unable to get data."} } return &posts, nil } func (db *datastore) GetAnonymousPosts(u *User, page int) (*[]PublicPost, error) { pagePosts := 10 start := page*pagePosts - pagePosts if page == 0 { start = 0 pagePosts = 1000 } limitStr := "" if page > 0 { limitStr = fmt.Sprintf(" LIMIT %d, %d", start, pagePosts) } rows, err := db.Query("SELECT id, view_count, title, language, created, updated, content FROM posts WHERE owner_id = ? AND collection_id IS NULL ORDER BY created DESC"+limitStr, u.ID) if err != nil { log.Error("Failed selecting from posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user anonymous posts."} } defer rows.Close() posts := []PublicPost{} for rows.Next() { p := Post{} err = rows.Scan(&p.ID, &p.ViewCount, &p.Title, &p.Language, &p.Created, &p.Updated, &p.Content) if err != nil { log.Error("Failed scanning row: %v", err) break } p.extractData() posts = append(posts, p.processPost()) } err = rows.Err() if err != nil { log.Error("Error after Next() on rows: %v", err) } return &posts, nil } func (db *datastore) GetUserPosts(u *User) (*[]PublicPost, error) { rows, err := db.Query("SELECT p.id, p.slug, p.view_count, p.title, p.created, p.updated, p.content, p.text_appearance, p.language, p.rtl, c.alias, c.title, c.description, c.view_count FROM posts p LEFT JOIN collections c ON collection_id = c.id WHERE p.owner_id = ? ORDER BY created ASC", u.ID) if err != nil { log.Error("Failed selecting from posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user posts."} } defer rows.Close() posts := []PublicPost{} var gotErr bool for rows.Next() { p := Post{} c := Collection{} var alias, title, description sql.NullString var views sql.NullInt64 err = rows.Scan(&p.ID, &p.Slug, &p.ViewCount, &p.Title, &p.Created, &p.Updated, &p.Content, &p.Font, &p.Language, &p.RTL, &alias, &title, &description, &views) if err != nil { log.Error("Failed scanning User.getPosts() row: %v", err) gotErr = true break } p.extractData() pubPost := p.processPost() if alias.Valid && alias.String != "" { c.Alias = alias.String c.Title = title.String c.Description = description.String c.Views = views.Int64 pubPost.Collection = &CollectionObj{Collection: c} } posts = append(posts, pubPost) } err = rows.Err() if err != nil { log.Error("Error after Next() on rows: %v", err) } if gotErr && len(posts) == 0 { // There were a lot of errors return nil, impart.HTTPError{http.StatusInternalServerError, "Unable to get data."} } return &posts, nil } func (db *datastore) GetUserPostsCount(userID int64) int64 { var count int64 err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE owner_id = ?", userID).Scan(&count) switch { case err == sql.ErrNoRows: return 0 case err != nil: log.Error("Failed selecting posts count for user %d: %v", userID, err) return 0 } return count } // ChangeSettings takes a User and applies the changes in the given // userSettings, MODIFYING THE USER with successful changes. func (db *datastore) ChangeSettings(app *App, u *User, s *userSettings) error { var errPass error q := query.NewUpdate() // Update email if given if s.Email != "" { encEmail, err := data.Encrypt(app.keys.EmailKey, s.Email) if err != nil { log.Error("Couldn't encrypt email %s: %s\n", s.Email, err) return impart.HTTPError{http.StatusInternalServerError, "Unable to encrypt email address."} } q.SetBytes(encEmail, "email") // Update the email if something goes awry updating the password defer func() { if errPass != nil { db.UpdateEncryptedUserEmail(u.ID, encEmail) } }() u.Email = zero.StringFrom(s.Email) } // Update username if given var newUsername string if s.Username != "" { var ie *impart.HTTPError newUsername, ie = getValidUsername(app, s.Username, u.Username) if ie != nil { // Username is invalid return *ie } if !author.IsValidUsername(app.cfg, newUsername) { // Ensure the username is syntactically correct. return impart.HTTPError{http.StatusPreconditionFailed, "Username isn't valid."} } t, err := db.Begin() if err != nil { log.Error("Couldn't start username change transaction: %v", err) return err } _, err = t.Exec("UPDATE users SET username = ? WHERE id = ?", newUsername, u.ID) if err != nil { t.Rollback() if db.isDuplicateKeyErr(err) { return impart.HTTPError{http.StatusConflict, "Username is already taken."} } log.Error("Unable to update users table: %v", err) return ErrInternalGeneral } _, err = t.Exec("UPDATE collections SET alias = ? WHERE alias = ? AND owner_id = ?", newUsername, u.Username, u.ID) if err != nil { t.Rollback() if db.isDuplicateKeyErr(err) { return impart.HTTPError{http.StatusConflict, "Username is already taken."} } log.Error("Unable to update collection: %v", err) return ErrInternalGeneral } // Keep track of name changes for redirection db.RemoveCollectionRedirect(t, newUsername) _, err = t.Exec("UPDATE collectionredirects SET new_alias = ? WHERE new_alias = ?", newUsername, u.Username) if err != nil { log.Error("Unable to update collectionredirects: %v", err) } _, err = t.Exec("INSERT INTO collectionredirects (prev_alias, new_alias) VALUES (?, ?)", u.Username, newUsername) if err != nil { log.Error("Unable to add new collectionredirect: %v", err) } err = t.Commit() if err != nil { t.Rollback() log.Error("Rolling back after Commit(): %v\n", err) return err } u.Username = newUsername } // Update passphrase if given if s.NewPass != "" { // Check if user has already set a password var err error u.HasPass, err = db.IsUserPassSet(u.ID) if err != nil { errPass = impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data."} return errPass } if u.HasPass { // Check if currently-set password is correct hashedPass := u.HashedPass if len(hashedPass) == 0 { authUser, err := db.GetUserForAuthByID(u.ID) if err != nil { errPass = err return errPass } hashedPass = authUser.HashedPass } if !auth.Authenticated(hashedPass, []byte(s.OldPass)) { errPass = impart.HTTPError{http.StatusUnauthorized, "Incorrect password."} return errPass } } hashedPass, err := auth.HashPass([]byte(s.NewPass)) if err != nil { errPass = impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."} return errPass } q.SetBytes(hashedPass, "password") } // WHERE values q.Append(u.ID) if q.Updates == "" { if s.Username == "" { return ErrPostNoUpdatableVals } // Nothing to update except username. That was successful, so return now. return nil } res, err := db.Exec("UPDATE users SET "+q.Updates+" WHERE id = ?", q.Params...) if err != nil { log.Error("Unable to update collection: %v", err) return err } rowsAffected, _ := res.RowsAffected() if rowsAffected == 0 { // Show the correct error message if nothing was updated var dummy int err := db.QueryRow("SELECT 1 FROM users WHERE id = ?", u.ID).Scan(&dummy) switch { case err == sql.ErrNoRows: return ErrUnauthorizedGeneral case err != nil: log.Error("Failed selecting from users: %v", err) } return nil } if s.NewPass != "" && !u.HasPass { u.HasPass = true } return nil } func (db *datastore) ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error { var dbPass []byte err := db.QueryRow("SELECT password FROM users WHERE id = ?", userID).Scan(&dbPass) switch { case err == sql.ErrNoRows: return ErrUserNotFound case err != nil: log.Error("Couldn't SELECT user password for change: %v", err) return err } if !sudo && !auth.Authenticated(dbPass, []byte(curPass)) { return impart.HTTPError{http.StatusUnauthorized, "Incorrect password."} } _, err = db.Exec("UPDATE users SET password = ? WHERE id = ?", hashedPass, userID) if err != nil { log.Error("Could not update passphrase: %v", err) return err } return nil } func (db *datastore) RemoveCollectionRedirect(t *sql.Tx, alias string) error { _, err := t.Exec("DELETE FROM collectionredirects WHERE prev_alias = ?", alias) if err != nil { log.Error("Unable to delete from collectionredirects: %v", err) return err } return nil } func (db *datastore) GetCollectionRedirect(alias string) (new string) { row := db.QueryRow("SELECT new_alias FROM collectionredirects WHERE prev_alias = ?", alias) err := row.Scan(&new) if err != nil && err != sql.ErrNoRows && !db.isIgnorableError(err) { log.Error("Failed selecting from collectionredirects: %v", err) } return } func (db *datastore) DeleteCollection(alias string, userID int64) error { c := &Collection{Alias: alias} var username string row := db.QueryRow("SELECT username FROM users WHERE id = ?", userID) err := row.Scan(&username) if err != nil { return err } // Ensure user isn't deleting their main blog if alias == username { return impart.HTTPError{http.StatusForbidden, "You cannot currently delete your primary blog."} } row = db.QueryRow("SELECT id FROM collections WHERE alias = ? AND owner_id = ?", alias, userID) err = row.Scan(&c.ID) switch { case err == sql.ErrNoRows: return impart.HTTPError{http.StatusNotFound, "Collection doesn't exist or you're not allowed to delete it."} case err != nil: log.Error("Failed selecting from collections: %v", err) return ErrInternalGeneral } t, err := db.Begin() if err != nil { return err } // Float all collection's posts _, err = t.Exec("UPDATE posts SET collection_id = NULL WHERE collection_id = ? AND owner_id = ?", c.ID, userID) if err != nil { t.Rollback() return err } // Remove redirects to or from this collection _, err = t.Exec("DELETE FROM collectionredirects WHERE prev_alias = ? OR new_alias = ?", alias, alias) if err != nil { t.Rollback() return err } // Remove any optional collection password _, err = t.Exec("DELETE FROM collectionpasswords WHERE collection_id = ?", c.ID) if err != nil { t.Rollback() return err } // Finally, delete collection itself _, err = t.Exec("DELETE FROM collections WHERE id = ?", c.ID) if err != nil { t.Rollback() return err } err = t.Commit() if err != nil { t.Rollback() return err } return nil } func (db *datastore) IsCollectionAttributeOn(id int64, attr string) bool { var v string err := db.QueryRow("SELECT value FROM collectionattributes WHERE collection_id = ? AND attribute = ?", id, attr).Scan(&v) switch { case err == sql.ErrNoRows: return false case err != nil: log.Error("Couldn't SELECT value in isCollectionAttributeOn for attribute '%s': %v", attr, err) return false } return v == "1" } func (db *datastore) CollectionHasAttribute(id int64, attr string) bool { var dummy string err := db.QueryRow("SELECT value FROM collectionattributes WHERE collection_id = ? AND attribute = ?", id, attr).Scan(&dummy) switch { case err == sql.ErrNoRows: return false case err != nil: log.Error("Couldn't SELECT value in collectionHasAttribute for attribute '%s': %v", attr, err) return false } return true } func (db *datastore) GetCollectionAttribute(id int64, attr string) string { var v string err := db.QueryRow("SELECT value FROM collectionattributes WHERE collection_id = ? AND attribute = ?", id, attr).Scan(&v) switch { case err == sql.ErrNoRows: return "" case err != nil: log.Error("Couldn't SELECT value in getCollectionAttribute for attribute '%s': %v", attr, err) return "" } return v } func (db *datastore) SetCollectionAttribute(id int64, attr, v string) error { _, err := db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) "+db.upsert("collection_id", "attribute")+" value = ?", id, attr, v, v) if err != nil { log.Error("Unable to INSERT into collectionattributes: %v", err) return err } return nil } // DeleteAccount will delete the entire account for userID func (db *datastore) DeleteAccount(userID int64) error { // Get all collections rows, err := db.Query("SELECT id, alias FROM collections WHERE owner_id = ?", userID) if err != nil { log.Error("Unable to get collections: %v", err) return err } defer rows.Close() colls := []Collection{} var c Collection for rows.Next() { err = rows.Scan(&c.ID, &c.Alias) if err != nil { log.Error("Unable to scan collection cols: %v", err) return err } colls = append(colls, c) } // Start transaction t, err := db.Begin() if err != nil { log.Error("Unable to begin: %v", err) return err } // Clean up all collection related information var res sql.Result for _, c := range colls { // Delete tokens res, err = t.Exec("DELETE FROM collectionattributes WHERE collection_id = ?", c.ID) if err != nil { t.Rollback() log.Error("Unable to delete attributes on %s: %v", c.Alias, err) return err } rs, _ := res.RowsAffected() log.Info("Deleted %d for %s from collectionattributes", rs, c.Alias) // Remove any optional collection password res, err = t.Exec("DELETE FROM collectionpasswords WHERE collection_id = ?", c.ID) if err != nil { t.Rollback() log.Error("Unable to delete passwords on %s: %v", c.Alias, err) return err } rs, _ = res.RowsAffected() log.Info("Deleted %d for %s from collectionpasswords", rs, c.Alias) // Remove redirects to this collection res, err = t.Exec("DELETE FROM collectionredirects WHERE new_alias = ?", c.Alias) if err != nil { t.Rollback() log.Error("Unable to delete redirects on %s: %v", c.Alias, err) return err } rs, _ = res.RowsAffected() log.Info("Deleted %d for %s from collectionredirects", rs, c.Alias) // Remove any collection keys res, err = t.Exec("DELETE FROM collectionkeys WHERE collection_id = ?", c.ID) if err != nil { t.Rollback() log.Error("Unable to delete keys on %s: %v", c.Alias, err) return err } rs, _ = res.RowsAffected() log.Info("Deleted %d for %s from collectionkeys", rs, c.Alias) // TODO: federate delete collection // Remove remote follows res, err = t.Exec("DELETE FROM remotefollows WHERE collection_id = ?", c.ID) if err != nil { t.Rollback() log.Error("Unable to delete remote follows on %s: %v", c.Alias, err) return err } rs, _ = res.RowsAffected() log.Info("Deleted %d for %s from remotefollows", rs, c.Alias) } // Delete collections res, err = t.Exec("DELETE FROM collections WHERE owner_id = ?", userID) if err != nil { t.Rollback() log.Error("Unable to delete collections: %v", err) return err } rs, _ := res.RowsAffected() log.Info("Deleted %d from collections", rs) // Delete tokens res, err = t.Exec("DELETE FROM accesstokens WHERE user_id = ?", userID) if err != nil { t.Rollback() log.Error("Unable to delete access tokens: %v", err) return err } rs, _ = res.RowsAffected() log.Info("Deleted %d from accesstokens", rs) // Delete user attributes res, err = t.Exec("DELETE FROM oauth_users WHERE user_id = ?", userID) if err != nil { t.Rollback() log.Error("Unable to delete oauth_users: %v", err) return err } rs, _ = res.RowsAffected() log.Info("Deleted %d from oauth_users", rs) // Delete posts // TODO: should maybe get each row so we can federate a delete // if so needs to be outside of transaction like collections res, err = t.Exec("DELETE FROM posts WHERE owner_id = ?", userID) if err != nil { t.Rollback() log.Error("Unable to delete posts: %v", err) return err } rs, _ = res.RowsAffected() log.Info("Deleted %d from posts", rs) // Delete user attributes res, err = t.Exec("DELETE FROM userattributes WHERE user_id = ?", userID) if err != nil { t.Rollback() log.Error("Unable to delete attributes: %v", err) return err } rs, _ = res.RowsAffected() log.Info("Deleted %d from userattributes", rs) // Delete user invites res, err = t.Exec("DELETE FROM userinvites WHERE owner_id = ?", userID) if err != nil { t.Rollback() log.Error("Unable to delete invites: %v", err) return err } rs, _ = res.RowsAffected() log.Info("Deleted %d from userinvites", rs) // Delete the user res, err = t.Exec("DELETE FROM users WHERE id = ?", userID) if err != nil { t.Rollback() log.Error("Unable to delete user: %v", err) return err } rs, _ = res.RowsAffected() log.Info("Deleted %d from users", rs) // Commit all changes to the database err = t.Commit() if err != nil { t.Rollback() log.Error("Unable to commit: %v", err) return err } // TODO: federate delete actor return nil } func (db *datastore) GetAPActorKeys(collectionID int64) ([]byte, []byte) { var pub, priv []byte err := db.QueryRow("SELECT public_key, private_key FROM collectionkeys WHERE collection_id = ?", collectionID).Scan(&pub, &priv) switch { case err == sql.ErrNoRows: // Generate keys pub, priv = activitypub.GenerateKeys() _, err = db.Exec("INSERT INTO collectionkeys (collection_id, public_key, private_key) VALUES (?, ?, ?)", collectionID, pub, priv) if err != nil { log.Error("Unable to INSERT new activitypub keypair: %v", err) return nil, nil } case err != nil: log.Error("Couldn't SELECT collectionkeys: %v", err) return nil, nil } return pub, priv } func (db *datastore) CreateUserInvite(id string, userID int64, maxUses int, expires *time.Time) error { _, err := db.Exec("INSERT INTO userinvites (id, owner_id, max_uses, created, expires, inactive) VALUES (?, ?, ?, "+db.now()+", ?, 0)", id, userID, maxUses, expires) return err } func (db *datastore) GetUserInvites(userID int64) (*[]Invite, error) { rows, err := db.Query("SELECT id, max_uses, created, expires, inactive FROM userinvites WHERE owner_id = ? ORDER BY created DESC", userID) if err != nil { log.Error("Failed selecting from userinvites: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user invites."} } defer rows.Close() is := []Invite{} for rows.Next() { i := Invite{} err = rows.Scan(&i.ID, &i.MaxUses, &i.Created, &i.Expires, &i.Inactive) is = append(is, i) } return &is, nil } func (db *datastore) GetUserInvite(id string) (*Invite, error) { var i Invite err := db.QueryRow("SELECT id, max_uses, created, expires, inactive FROM userinvites WHERE id = ?", id).Scan(&i.ID, &i.MaxUses, &i.Created, &i.Expires, &i.Inactive) switch { case err == sql.ErrNoRows, db.isIgnorableError(err): return nil, impart.HTTPError{http.StatusNotFound, "Invite doesn't exist."} case err != nil: log.Error("Failed selecting invite: %v", err) return nil, err } return &i, nil } // IsUsersInvite returns true if the user with ID created the invite with code // and an error other than sql no rows, if any. Will return false in the event // of an error. func (db *datastore) IsUsersInvite(code string, userID int64) (bool, error) { var id string err := db.QueryRow("SELECT id FROM userinvites WHERE id = ? AND owner_id = ?", code, userID).Scan(&id) if err != nil && err != sql.ErrNoRows { log.Error("Failed selecting invite: %v", err) return false, err } return id != "", nil } func (db *datastore) GetUsersInvitedCount(id string) int64 { var count int64 err := db.QueryRow("SELECT COUNT(*) FROM usersinvited WHERE invite_id = ?", id).Scan(&count) switch { case err == sql.ErrNoRows: return 0 case err != nil: log.Error("Failed selecting users invited count: %v", err) return 0 } return count } func (db *datastore) CreateInvitedUser(inviteID string, userID int64) error { _, err := db.Exec("INSERT INTO usersinvited (invite_id, user_id) VALUES (?, ?)", inviteID, userID) return err } func (db *datastore) GetInstancePages() ([]*instanceContent, error) { return db.GetAllDynamicContent("page") } func (db *datastore) GetAllDynamicContent(t string) ([]*instanceContent, error) { where := "" params := []interface{}{} if t != "" { where = " WHERE content_type = ?" params = append(params, t) } rows, err := db.Query("SELECT id, title, content, updated, content_type FROM appcontent"+where, params...) if err != nil { log.Error("Failed selecting from appcontent: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve instance pages."} } defer rows.Close() pages := []*instanceContent{} for rows.Next() { c := &instanceContent{} err = rows.Scan(&c.ID, &c.Title, &c.Content, &c.Updated, &c.Type) if err != nil { log.Error("Failed scanning row: %v", err) break } pages = append(pages, c) } err = rows.Err() if err != nil { log.Error("Error after Next() on rows: %v", err) } return pages, nil } func (db *datastore) GetDynamicContent(id string) (*instanceContent, error) { c := &instanceContent{ ID: id, } err := db.QueryRow("SELECT title, content, updated, content_type FROM appcontent WHERE id = ?", id).Scan(&c.Title, &c.Content, &c.Updated, &c.Type) switch { case err == sql.ErrNoRows: return nil, nil case err != nil: log.Error("Couldn't SELECT FROM appcontent for id '%s': %v", id, err) return nil, err } return c, nil } func (db *datastore) UpdateDynamicContent(id, title, content, contentType string) error { var err error if db.driverName == driverSQLite { _, err = db.Exec("INSERT OR REPLACE INTO appcontent (id, title, content, updated, content_type) VALUES (?, ?, ?, "+db.now()+", ?)", id, title, content, contentType) } else { _, err = db.Exec("INSERT INTO appcontent (id, title, content, updated, content_type) VALUES (?, ?, ?, "+db.now()+", ?) "+db.upsert("id")+" title = ?, content = ?, updated = "+db.now(), id, title, content, contentType, title, content) } if err != nil { log.Error("Unable to INSERT appcontent for '%s': %v", id, err) } return err } func (db *datastore) GetAllUsers(page uint) (*[]User, error) { limitStr := fmt.Sprintf("0, %d", adminUsersPerPage) if page > 1 { limitStr = fmt.Sprintf("%d, %d", (page-1)*adminUsersPerPage, adminUsersPerPage) } rows, err := db.Query("SELECT id, username, created, status FROM users ORDER BY created DESC LIMIT " + limitStr) if err != nil { log.Error("Failed selecting from users: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve all users."} } defer rows.Close() users := []User{} for rows.Next() { u := User{} err = rows.Scan(&u.ID, &u.Username, &u.Created, &u.Status) if err != nil { log.Error("Failed scanning GetAllUsers() row: %v", err) break } users = append(users, u) } return &users, nil } func (db *datastore) GetAllUsersCount() int64 { var count int64 err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count) switch { case err == sql.ErrNoRows: return 0 case err != nil: log.Error("Failed selecting all users count: %v", err) return 0 } return count } func (db *datastore) GetUserLastPostTime(id int64) (*time.Time, error) { var t time.Time err := db.QueryRow("SELECT created FROM posts WHERE owner_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t) switch { case err == sql.ErrNoRows: return nil, nil case err != nil: log.Error("Failed selecting last post time from posts: %v", err) return nil, err } return &t, nil } // SetUserStatus changes a user's status in the database. see Users.UserStatus func (db *datastore) SetUserStatus(id int64, status UserStatus) error { _, err := db.Exec("UPDATE users SET status = ? WHERE id = ?", status, id) if err != nil { return fmt.Errorf("failed to update user status: %v", err) } return nil } func (db *datastore) GetCollectionLastPostTime(id int64) (*time.Time, error) { var t time.Time err := db.QueryRow("SELECT created FROM posts WHERE collection_id = ? ORDER BY created DESC LIMIT 1", id).Scan(&t) switch { case err == sql.ErrNoRows: return nil, nil case err != nil: log.Error("Failed selecting last post time from posts: %v", err) return nil, err } return &t, nil } func (db *datastore) GenerateOAuthState(ctx context.Context, provider string, clientID string, attachUser int64, inviteCode string) (string, error) { state := id.Generate62RandomString(24) attachUserVal := sql.NullInt64{Valid: attachUser > 0, Int64: attachUser} inviteCodeVal := sql.NullString{Valid: inviteCode != "", String: inviteCode} _, err := db.ExecContext(ctx, "INSERT INTO oauth_client_states (state, provider, client_id, used, created_at, attach_user_id, invite_code) VALUES (?, ?, ?, FALSE, "+db.now()+", ?, ?)", state, provider, clientID, attachUserVal, inviteCodeVal) if err != nil { return "", fmt.Errorf("unable to record oauth client state: %w", err) } return state, nil } func (db *datastore) ValidateOAuthState(ctx context.Context, state string) (string, string, int64, string, error) { var provider string var clientID string var attachUserID sql.NullInt64 var inviteCode sql.NullString err := wf_db.RunTransactionWithOptions(ctx, db.DB, &sql.TxOptions{}, func(ctx context.Context, tx *sql.Tx) error { err := tx. QueryRowContext(ctx, "SELECT provider, client_id, attach_user_id, invite_code FROM oauth_client_states WHERE state = ? AND used = FALSE", state). Scan(&provider, &clientID, &attachUserID, &inviteCode) if err != nil { return err } res, err := tx.ExecContext(ctx, "UPDATE oauth_client_states SET used = TRUE WHERE state = ?", state) if err != nil { return err } rowsAffected, err := res.RowsAffected() if err != nil { return err } if rowsAffected != 1 { return fmt.Errorf("state not found") } return nil }) if err != nil { return "", "", 0, "", nil } return provider, clientID, attachUserID.Int64, inviteCode.String, nil } func (db *datastore) RecordRemoteUserID(ctx context.Context, localUserID int64, remoteUserID, provider, clientID, accessToken string) error { var err error if db.driverName == driverSQLite { _, err = db.ExecContext(ctx, "INSERT OR REPLACE INTO oauth_users (user_id, remote_user_id, provider, client_id, access_token) VALUES (?, ?, ?, ?, ?)", localUserID, remoteUserID, provider, clientID, accessToken) } else { _, err = db.ExecContext(ctx, "INSERT INTO oauth_users (user_id, remote_user_id, provider, client_id, access_token) VALUES (?, ?, ?, ?, ?) "+db.upsert("user")+" access_token = ?", localUserID, remoteUserID, provider, clientID, accessToken, accessToken) } if err != nil { log.Error("Unable to INSERT oauth_users for '%d': %v", localUserID, err) } return err } // GetIDForRemoteUser returns a user ID associated with a remote user ID. func (db *datastore) GetIDForRemoteUser(ctx context.Context, remoteUserID, provider, clientID string) (int64, error) { var userID int64 = -1 err := db. QueryRowContext(ctx, "SELECT user_id FROM oauth_users WHERE remote_user_id = ? AND provider = ? AND client_id = ?", remoteUserID, provider, clientID). Scan(&userID) // Not finding a record is OK. if err != nil && err != sql.ErrNoRows { return -1, err } return userID, nil } type oauthAccountInfo struct { Provider string ClientID string RemoteUserID string DisplayName string AllowDisconnect bool } func (db *datastore) GetOauthAccounts(ctx context.Context, userID int64) ([]oauthAccountInfo, error) { rows, err := db.QueryContext(ctx, "SELECT provider, client_id, remote_user_id FROM oauth_users WHERE user_id = ? ", userID) if err != nil { log.Error("Failed selecting from oauth_users: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user oauth accounts."} } defer rows.Close() var records []oauthAccountInfo for rows.Next() { info := oauthAccountInfo{} err = rows.Scan(&info.Provider, &info.ClientID, &info.RemoteUserID) if err != nil { log.Error("Failed scanning GetAllUsers() row: %v", err) break } records = append(records, info) } return records, nil } // DatabaseInitialized returns whether or not the current datastore has been // initialized with the correct schema. // Currently, it checks to see if the `users` table exists. func (db *datastore) DatabaseInitialized() bool { var dummy string var err error if db.driverName == driverSQLite { err = db.QueryRow("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'users'").Scan(&dummy) } else { err = db.QueryRow("SHOW TABLES LIKE 'users'").Scan(&dummy) } switch { case err == sql.ErrNoRows: return false case err != nil: log.Error("Couldn't SHOW TABLES: %v", err) return false } return true } func (db *datastore) RemoveOauth(ctx context.Context, userID int64, provider string, clientID string, remoteUserID string) error { _, err := db.ExecContext(ctx, `DELETE FROM oauth_users WHERE user_id = ? AND provider = ? AND client_id = ? AND remote_user_id = ?`, userID, provider, clientID, remoteUserID) return err } func stringLogln(log *string, s string, v ...interface{}) { *log += fmt.Sprintf(s+"\n", v...) } func handleFailedPostInsert(err error) error { log.Error("Couldn't insert into posts: %v", err) return err } // Deprecated: use GetProfileURLFromHandle() instead, which returns user-facing URL instead of actor_id func (db *datastore) GetProfilePageFromHandle(app *App, handle string) (string, error) { handle = strings.TrimLeft(handle, "@") actorIRI := "" parts := strings.Split(handle, "@") if len(parts) != 2 { return "", fmt.Errorf("invalid handle format") } domain := parts[1] // Check non-AP instances if siloProfileURL := silobridge.Profile(parts[0], domain); siloProfileURL != "" { return siloProfileURL, nil } remoteUser, err := getRemoteUserFromHandle(app, handle) if err != nil { // can't find using handle in the table but the table may already have this user without // handle from a previous version // TODO: Make this determination. We should know whether a user exists without a handle, or doesn't exist at all actorIRI = RemoteLookup(handle) _, errRemoteUser := getRemoteUser(app, actorIRI) // if it exists then we need to update the handle if errRemoteUser == nil { _, err := app.db.Exec("UPDATE remoteusers SET handle = ? WHERE actor_id = ?", handle, actorIRI) if err != nil { log.Error("Couldn't update handle '%s' for user %s", handle, actorIRI) } } else { // this probably means we don't have the user in the table so let's try to insert it // here we need to ask the server for the inboxes remoteActor, err := activityserve.NewRemoteActor(actorIRI) if err != nil { log.Error("Couldn't fetch remote actor: %v", err) } if debugging { log.Info("%s %s %s %s", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), handle) } _, err = app.db.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox, handle) VALUES(?, ?, ?, ?)", actorIRI, remoteActor.GetInbox(), remoteActor.GetSharedInbox(), handle) if err != nil { log.Error("Couldn't insert remote user: %v", err) return "", err } } } else { actorIRI = remoteUser.ActorID } return actorIRI, nil } func (db *datastore) AddEmailSubscription(collID, userID int64, email string, confirmed bool) (*EmailSubscriber, error) { friendlyChars := "0123456789BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz" subID := id.GenerateRandomString(friendlyChars, 8) token := id.GenerateRandomString(friendlyChars, 16) emailVal := sql.NullString{ String: email, Valid: email != "", } userIDVal := sql.NullInt64{ Int64: userID, Valid: userID > 0, } _, err := db.Exec("INSERT INTO emailsubscribers (id, collection_id, user_id, email, subscribed, token, confirmed) VALUES (?, ?, ?, ?, "+db.now()+", ?, ?)", subID, collID, userIDVal, emailVal, token, confirmed) if err != nil { if mysqlErr, ok := err.(*mysql.MySQLError); ok { if mysqlErr.Number == mySQLErrDuplicateKey { // Duplicate, so just return existing subscriber information log.Info("Duplicate subscriber for email %s, user %d; returning existing subscriber", email, userID) return db.FetchEmailSubscriber(email, userID, collID) } } return nil, err } return &EmailSubscriber{ ID: subID, CollID: collID, UserID: userIDVal, Email: emailVal, Token: token, }, nil } func (db *datastore) IsEmailSubscriber(email string, userID, collID int64) bool { var dummy int var err error if email != "" { err = db.QueryRow("SELECT 1 FROM emailsubscribers WHERE email = ? AND collection_id = ?", email, collID).Scan(&dummy) } else { err = db.QueryRow("SELECT 1 FROM emailsubscribers WHERE user_id = ? AND collection_id = ?", userID, collID).Scan(&dummy) } switch { case err == sql.ErrNoRows: return false case err != nil: return false } return true } func (db *datastore) GetEmailSubscribers(collID int64, reqConfirmed bool) ([]*EmailSubscriber, error) { cond := "" if reqConfirmed { cond = " AND confirmed = 1" } rows, err := db.Query(`SELECT s.id, collection_id, user_id, s.email, u.email, subscribed, token, confirmed, allow_export FROM emailsubscribers s LEFT JOIN users u ON u.id = user_id WHERE collection_id = ?`+cond+` ORDER BY subscribed DESC`, collID) if err != nil { log.Error("Failed selecting email subscribers for collection %d: %v", collID, err) return nil, err } defer rows.Close() var subs []*EmailSubscriber for rows.Next() { s := &EmailSubscriber{} err = rows.Scan(&s.ID, &s.CollID, &s.UserID, &s.Email, &s.acctEmail, &s.Subscribed, &s.Token, &s.Confirmed, &s.AllowExport) if err != nil { log.Error("Failed scanning row from email subscribers: %v", err) continue } subs = append(subs, s) } return subs, nil } func (db *datastore) FetchEmailSubscriberEmail(subID, token string) (string, error) { var email sql.NullString // TODO: return user email if there's a user_id ? err := db.QueryRow("SELECT email FROM emailsubscribers WHERE id = ? AND token = ?", subID, token).Scan(&email) switch { case err == sql.ErrNoRows: return "", fmt.Errorf("Subscriber doesn't exist or token is invalid.") case err != nil: log.Error("Couldn't SELECT email from emailsubscribers: %v", err) return "", fmt.Errorf("Something went very wrong.") } return email.String, nil } func (db *datastore) FetchEmailSubscriber(email string, userID, collID int64) (*EmailSubscriber, error) { const emailSubCols = "id, collection_id, user_id, email, subscribed, token, confirmed, allow_export" s := &EmailSubscriber{} var row *sql.Row if email != "" { row = db.QueryRow("SELECT "+emailSubCols+" FROM emailsubscribers WHERE email = ? AND collection_id = ?", email, collID) } else { row = db.QueryRow("SELECT "+emailSubCols+" FROM emailsubscribers WHERE user_id = ? AND collection_id = ?", userID, collID) } err := row.Scan(&s.ID, &s.CollID, &s.UserID, &s.Email, &s.Subscribed, &s.Token, &s.Confirmed, &s.AllowExport) switch { case err == sql.ErrNoRows: return nil, nil case err != nil: return nil, err } return s, nil } func (db *datastore) DeleteEmailSubscriber(subID, token string) error { res, err := db.Exec("DELETE FROM emailsubscribers WHERE id = ? AND token = ?", subID, token) if err != nil { return err } rowsAffected, _ := res.RowsAffected() if rowsAffected == 0 { return impart.HTTPError{http.StatusNotFound, "Invalid token, or subscriber doesn't exist"} } return nil } func (db *datastore) DeleteEmailSubscriberByUser(email string, userID, collID int64) error { var res sql.Result var err error if email != "" { res, err = db.Exec("DELETE FROM emailsubscribers WHERE email = ? AND collection_id = ?", email, collID) } else { res, err = db.Exec("DELETE FROM emailsubscribers WHERE user_id = ? AND collection_id = ?", userID, collID) } if err != nil { return err } rowsAffected, _ := res.RowsAffected() if rowsAffected == 0 { return impart.HTTPError{http.StatusNotFound, "Subscriber doesn't exist"} } return nil } func (db *datastore) UpdateSubscriberConfirmed(subID, token string) error { email, err := db.FetchEmailSubscriberEmail(subID, token) if err != nil { log.Error("Didn't fetch email subscriber: %v", err) return err } // TODO: ensure all addresses with original name are also confirmed, e.g. matt+fake@write.as and matt@write.as are now confirmed _, err = db.Exec("UPDATE emailsubscribers SET confirmed = 1 WHERE email = ?", email) if err != nil { log.Error("Could not update email subscriber confirmation status: %v", err) return err } return nil } func (db *datastore) IsSubscriberConfirmed(email string) bool { var dummy int64 err := db.QueryRow("SELECT 1 FROM emailsubscribers WHERE email = ? AND confirmed = 1", email).Scan(&dummy) switch { case err == sql.ErrNoRows: return false case err != nil: log.Error("Couldn't SELECT in isSubscriberConfirmed: %v", err) return false } return true } func (db *datastore) InsertJob(j *PostJob) error { res, err := db.Exec("INSERT INTO publishjobs (post_id, action, delay) VALUES (?, ?, ?)", j.PostID, j.Action, j.Delay) if err != nil { return err } jobID, err := res.LastInsertId() if err != nil { log.Error("[jobs] Couldn't get last insert ID! %s", err) } log.Info("[jobs] Queued %s job #%d for post %s, delayed %d minutes", j.Action, jobID, j.PostID, j.Delay) return nil } func (db *datastore) UpdateJobForPost(postID string, delay int64) error { _, err := db.Exec("UPDATE publishjobs SET delay = ? WHERE post_id = ?", delay, postID) if err != nil { return fmt.Errorf("Unable to update publish job: %s", err) } log.Info("Updated job for post %s: delay %d", postID, delay) return nil } func (db *datastore) DeleteJob(id int64) error { _, err := db.Exec("DELETE FROM publishjobs WHERE id = ?", id) if err != nil { return err } log.Info("[job #%d] Deleted.", id) return nil } func (db *datastore) DeleteJobByPost(postID string) error { _, err := db.Exec("DELETE FROM publishjobs WHERE post_id = ?", postID) if err != nil { return err } log.Info("[job] Deleted job for post %s", postID) return nil } func (db *datastore) GetJobsToRun(action string) ([]*PostJob, error) { timeWhere := "created < DATE_SUB(NOW(), INTERVAL delay MINUTE) AND created > DATE_SUB(NOW(), INTERVAL delay + 5 MINUTE)" if db.driverName == driverSQLite { timeWhere = "created < DATETIME('now', '-' || delay || ' MINUTE') AND created > DATETIME('now', '-' || (delay+5) || ' MINUTE')" } rows, err := db.Query(`SELECT pj.id, post_id, action, delay FROM publishjobs pj INNER JOIN posts p ON post_id = p.id WHERE action = ? AND `+timeWhere+` ORDER BY created ASC`, action) if err != nil { log.Error("Failed selecting from publishjobs: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve publish jobs."} } defer rows.Close() jobs := []*PostJob{} for rows.Next() { j := &PostJob{} err = rows.Scan(&j.ID, &j.PostID, &j.Action, &j.Delay) jobs = append(jobs, j) } return jobs, nil } diff --git a/database_activitypub.go b/database_activitypub.go index b832d37..9df3724 100644 --- a/database_activitypub.go +++ b/database_activitypub.go @@ -1,50 +1,49 @@ /* * Copyright © 2024 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "database/sql" "fmt" - "github.com/writeas/web-core/activitystreams" "github.com/writeas/web-core/log" ) func apAddRemoteUser(app *App, t *sql.Tx, fullActor *activitystreams.Person) (int64, error) { // Add remote user locally, since it wasn't found before res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox, url) VALUES (?, ?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox, fullActor.URL) if err != nil { t.Rollback() return -1, fmt.Errorf("couldn't add new remoteuser in DB: %v", err) } remoteUserID, err := res.LastInsertId() if err != nil { t.Rollback() return -1, fmt.Errorf("no lastinsertid for followers, rolling back: %v", err) } // Add in key _, err = t.Exec("INSERT INTO remoteuserkeys (id, remote_user_id, public_key) VALUES (?, ?, ?)", fullActor.PublicKey.ID, remoteUserID, fullActor.PublicKey.PublicKeyPEM) if err != nil { if !app.db.isDuplicateKeyErr(err) { t.Rollback() log.Error("Couldn't add follower keys in DB: %v\n", err) return -1, fmt.Errorf("couldn't add follower keys in DB: %v", err) } else { t.Rollback() log.Error("Couldn't add follower keys in DB: %v\n", err) return -1, fmt.Errorf("couldn't add follower keys in DB: %v", err) } } return remoteUserID, nil } diff --git a/database_test.go b/database_test.go index fc0523c..c114077 100644 --- a/database_test.go +++ b/database_test.go @@ -1,51 +1,50 @@ package writefreely import ( "context" "database/sql" - "testing" - "github.com/stretchr/testify/assert" + "testing" ) func TestOAuthDatastore(t *testing.T) { if !runMySQLTests() { t.Skip("skipping mysql tests") } withTestDB(t, func(db *sql.DB) { ctx := context.Background() ds := &datastore{ DB: db, driverName: "", } state, err := ds.GenerateOAuthState(ctx, "test", "development", 0, "") assert.NoError(t, err) assert.Len(t, state, 24) countRows(t, ctx, db, 1, "SELECT COUNT(*) FROM `oauth_client_states` WHERE `state` = ? AND `used` = false", state) _, _, _, _, err = ds.ValidateOAuthState(ctx, state) assert.NoError(t, err) countRows(t, ctx, db, 1, "SELECT COUNT(*) FROM `oauth_client_states` WHERE `state` = ? AND `used` = true", state) var localUserID int64 = 99 var remoteUserID = "100" err = ds.RecordRemoteUserID(ctx, localUserID, remoteUserID, "test", "test", "access_token_a") assert.NoError(t, err) countRows(t, ctx, db, 1, "SELECT COUNT(*) FROM `oauth_users` WHERE `user_id` = ? AND `remote_user_id` = ? AND access_token = 'access_token_a'", localUserID, remoteUserID) err = ds.RecordRemoteUserID(ctx, localUserID, remoteUserID, "test", "test", "access_token_b") assert.NoError(t, err) countRows(t, ctx, db, 1, "SELECT COUNT(*) FROM `oauth_users` WHERE `user_id` = ? AND `remote_user_id` = ? AND access_token = 'access_token_b'", localUserID, remoteUserID) countRows(t, ctx, db, 1, "SELECT COUNT(*) FROM `oauth_users`") foundUserID, err := ds.GetIDForRemoteUser(ctx, remoteUserID, "test", "test") assert.NoError(t, err) assert.Equal(t, localUserID, foundUserID) }) } diff --git a/db/create_test.go b/db/create_test.go index b971bdf..369d5c1 100644 --- a/db/create_test.go +++ b/db/create_test.go @@ -1,147 +1,146 @@ package db import ( - "testing" - "github.com/stretchr/testify/assert" + "testing" ) func TestDialect_Column(t *testing.T) { c1 := DialectSQLite.Column("foo", ColumnTypeBool, UnsetSize) assert.Equal(t, DialectSQLite, c1.Dialect) c2 := DialectMySQL.Column("foo", ColumnTypeBool, UnsetSize) assert.Equal(t, DialectMySQL, c2.Dialect) } func TestColumnType_Format(t *testing.T) { type args struct { dialect DialectType size OptionalInt } tests := []struct { name string d ColumnType args args want string wantErr bool }{ {"Sqlite bool", ColumnTypeBool, args{dialect: DialectSQLite}, "INTEGER", false}, {"Sqlite small int", ColumnTypeSmallInt, args{dialect: DialectSQLite}, "INTEGER", false}, {"Sqlite int", ColumnTypeInteger, args{dialect: DialectSQLite}, "INTEGER", false}, {"Sqlite char", ColumnTypeChar, args{dialect: DialectSQLite}, "TEXT", false}, {"Sqlite varchar", ColumnTypeVarChar, args{dialect: DialectSQLite}, "TEXT", false}, {"Sqlite text", ColumnTypeText, args{dialect: DialectSQLite}, "TEXT", false}, {"Sqlite datetime", ColumnTypeDateTime, args{dialect: DialectSQLite}, "DATETIME", false}, {"MySQL bool", ColumnTypeBool, args{dialect: DialectMySQL}, "TINYINT(1)", false}, {"MySQL small int", ColumnTypeSmallInt, args{dialect: DialectMySQL}, "SMALLINT", false}, {"MySQL small int with param", ColumnTypeSmallInt, args{dialect: DialectMySQL, size: OptionalInt{true, 3}}, "SMALLINT(3)", false}, {"MySQL int", ColumnTypeInteger, args{dialect: DialectMySQL}, "INT", false}, {"MySQL int with param", ColumnTypeInteger, args{dialect: DialectMySQL, size: OptionalInt{true, 11}}, "INT(11)", false}, {"MySQL char", ColumnTypeChar, args{dialect: DialectMySQL}, "CHAR", false}, {"MySQL char with param", ColumnTypeChar, args{dialect: DialectMySQL, size: OptionalInt{true, 4}}, "CHAR(4)", false}, {"MySQL varchar", ColumnTypeVarChar, args{dialect: DialectMySQL}, "VARCHAR", false}, {"MySQL varchar with param", ColumnTypeVarChar, args{dialect: DialectMySQL, size: OptionalInt{true, 25}}, "VARCHAR(25)", false}, {"MySQL text", ColumnTypeText, args{dialect: DialectMySQL}, "TEXT", false}, {"MySQL datetime", ColumnTypeDateTime, args{dialect: DialectMySQL}, "DATETIME", false}, {"invalid column type", 10000, args{dialect: DialectMySQL}, "", true}, {"invalid dialect", ColumnTypeBool, args{dialect: 10000}, "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := tt.d.Format(tt.args.dialect, tt.args.size) if (err != nil) != tt.wantErr { t.Errorf("Format() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("Format() got = %v, want %v", got, tt.want) } }) } } func TestColumn_Build(t *testing.T) { type fields struct { Dialect DialectType Name string Nullable bool Default OptionalString Type ColumnType Size OptionalInt PrimaryKey bool } tests := []struct { name string fields fields want string wantErr bool }{ {"Sqlite bool", fields{DialectSQLite, "foo", false, UnsetDefault, ColumnTypeBool, UnsetSize, false}, "foo INTEGER NOT NULL", false}, {"Sqlite bool nullable", fields{DialectSQLite, "foo", true, UnsetDefault, ColumnTypeBool, UnsetSize, false}, "foo INTEGER", false}, {"Sqlite small int", fields{DialectSQLite, "foo", false, UnsetDefault, ColumnTypeSmallInt, UnsetSize, true}, "foo INTEGER NOT NULL PRIMARY KEY", false}, {"Sqlite small int nullable", fields{DialectSQLite, "foo", true, UnsetDefault, ColumnTypeSmallInt, UnsetSize, false}, "foo INTEGER", false}, {"Sqlite int", fields{DialectSQLite, "foo", false, UnsetDefault, ColumnTypeInteger, UnsetSize, false}, "foo INTEGER NOT NULL", false}, {"Sqlite int nullable", fields{DialectSQLite, "foo", true, UnsetDefault, ColumnTypeInteger, UnsetSize, false}, "foo INTEGER", false}, {"Sqlite char", fields{DialectSQLite, "foo", false, UnsetDefault, ColumnTypeChar, UnsetSize, false}, "foo TEXT NOT NULL", false}, {"Sqlite char nullable", fields{DialectSQLite, "foo", true, UnsetDefault, ColumnTypeChar, UnsetSize, false}, "foo TEXT", false}, {"Sqlite varchar", fields{DialectSQLite, "foo", false, UnsetDefault, ColumnTypeVarChar, UnsetSize, false}, "foo TEXT NOT NULL", false}, {"Sqlite varchar nullable", fields{DialectSQLite, "foo", true, UnsetDefault, ColumnTypeVarChar, UnsetSize, false}, "foo TEXT", false}, {"Sqlite text", fields{DialectSQLite, "foo", false, UnsetDefault, ColumnTypeText, UnsetSize, false}, "foo TEXT NOT NULL", false}, {"Sqlite text nullable", fields{DialectSQLite, "foo", true, UnsetDefault, ColumnTypeText, UnsetSize, false}, "foo TEXT", false}, {"Sqlite datetime", fields{DialectSQLite, "foo", false, UnsetDefault, ColumnTypeDateTime, UnsetSize, false}, "foo DATETIME NOT NULL", false}, {"Sqlite datetime nullable", fields{DialectSQLite, "foo", true, UnsetDefault, ColumnTypeDateTime, UnsetSize, false}, "foo DATETIME", false}, {"MySQL bool", fields{DialectMySQL, "foo", false, UnsetDefault, ColumnTypeBool, UnsetSize, false}, "foo TINYINT(1) NOT NULL", false}, {"MySQL bool nullable", fields{DialectMySQL, "foo", true, UnsetDefault, ColumnTypeBool, UnsetSize, false}, "foo TINYINT(1)", false}, {"MySQL small int", fields{DialectMySQL, "foo", false, UnsetDefault, ColumnTypeSmallInt, UnsetSize, true}, "foo SMALLINT NOT NULL PRIMARY KEY", false}, {"MySQL small int nullable", fields{DialectMySQL, "foo", true, UnsetDefault, ColumnTypeSmallInt, UnsetSize, false}, "foo SMALLINT", false}, {"MySQL int", fields{DialectMySQL, "foo", false, UnsetDefault, ColumnTypeInteger, UnsetSize, false}, "foo INT NOT NULL", false}, {"MySQL int nullable", fields{DialectMySQL, "foo", true, UnsetDefault, ColumnTypeInteger, UnsetSize, false}, "foo INT", false}, {"MySQL char", fields{DialectMySQL, "foo", false, UnsetDefault, ColumnTypeChar, UnsetSize, false}, "foo CHAR NOT NULL", false}, {"MySQL char nullable", fields{DialectMySQL, "foo", true, UnsetDefault, ColumnTypeChar, UnsetSize, false}, "foo CHAR", false}, {"MySQL varchar", fields{DialectMySQL, "foo", false, UnsetDefault, ColumnTypeVarChar, UnsetSize, false}, "foo VARCHAR NOT NULL", false}, {"MySQL varchar nullable", fields{DialectMySQL, "foo", true, UnsetDefault, ColumnTypeVarChar, UnsetSize, false}, "foo VARCHAR", false}, {"MySQL text", fields{DialectMySQL, "foo", false, UnsetDefault, ColumnTypeText, UnsetSize, false}, "foo TEXT NOT NULL", false}, {"MySQL text nullable", fields{DialectMySQL, "foo", true, UnsetDefault, ColumnTypeText, UnsetSize, false}, "foo TEXT", false}, {"MySQL datetime", fields{DialectMySQL, "foo", false, UnsetDefault, ColumnTypeDateTime, UnsetSize, false}, "foo DATETIME NOT NULL", false}, {"MySQL datetime nullable", fields{DialectMySQL, "foo", true, UnsetDefault, ColumnTypeDateTime, UnsetSize, false}, "foo DATETIME", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Column{ Dialect: tt.fields.Dialect, Name: tt.fields.Name, Nullable: tt.fields.Nullable, Default: tt.fields.Default, Type: tt.fields.Type, Size: tt.fields.Size, PrimaryKey: tt.fields.PrimaryKey, } if got, err := c.String(); got != tt.want { if (err != nil) != tt.wantErr { t.Errorf("String() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("String() got = %v, want %v", got, tt.want) } } }) } } func TestCreateTableSqlBuilder_ToSQL(t *testing.T) { sql, err := DialectMySQL. Table("foo"). SetIfNotExists(true). Column(DialectMySQL.Column("bar", ColumnTypeInteger, UnsetSize).SetPrimaryKey(true)). Column(DialectMySQL.Column("baz", ColumnTypeText, UnsetSize)). Column(DialectMySQL.Column("qux", ColumnTypeDateTime, UnsetSize).SetDefault("NOW()")). UniqueConstraint("bar"). UniqueConstraint("bar", "baz"). ToSQL() assert.NoError(t, err) assert.Equal(t, "CREATE TABLE IF NOT EXISTS foo ( bar INT NOT NULL PRIMARY KEY, baz TEXT NOT NULL, qux DATETIME NOT NULL DEFAULT NOW(), UNIQUE(bar), UNIQUE(bar,baz) )", sql) } diff --git a/email.go b/email.go index f2332df..fd58c80 100644 --- a/email.go +++ b/email.go @@ -1,512 +1,525 @@ /* * Copyright © 2019-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "bytes" "database/sql" "encoding/json" "fmt" "html/template" "net/http" + "net/url" "strings" "time" + "github.com/writefreely/writefreely/mailer" + "github.com/aymerick/douceur/inliner" "github.com/gorilla/mux" stripmd "github.com/writeas/go-strip-markdown/v2" "github.com/writeas/impart" "github.com/writeas/web-core/data" "github.com/writeas/web-core/log" "github.com/writefreely/writefreely/key" - "github.com/writefreely/writefreely/mailer" "github.com/writefreely/writefreely/spam" ) const ( emailSendDelay = 15 ) type ( SubmittedSubscription struct { CollAlias string UserID int64 Email string `schema:"email" json:"email"` Web bool `schema:"web" json:"web"` Slug string `schema:"slug" json:"slug"` From string `schema:"from" json:"from"` } EmailSubscriber struct { ID string CollID int64 UserID sql.NullInt64 Email sql.NullString Subscribed time.Time Token string Confirmed bool AllowExport bool acctEmail sql.NullString } ) func (es *EmailSubscriber) FinalEmail(keys *key.Keychain) string { if !es.UserID.Valid || es.Email.Valid { return es.Email.String } decEmail, err := data.Decrypt(keys.EmailKey, []byte(es.acctEmail.String)) if err != nil { log.Error("Error decrypting user email: %v", err) return "" } return string(decEmail) } func (es *EmailSubscriber) SubscribedFriendly() string { return es.Subscribed.Format("January 2, 2006") } +func sanitizeSubscriptionForm(form url.Values) url.Values { + cleanForm := make(url.Values, len(form)) + for k, v := range form { + cleanForm[k] = v + } + delete(cleanForm, spam.HoneypotFieldName()) + delete(cleanForm, "fake_password") + return cleanForm +} + func handleCreateEmailSubscription(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r) vars := mux.Vars(r) var err error ss := SubmittedSubscription{ CollAlias: vars["alias"], } u := getUserSession(app, r) if u != nil { ss.UserID = u.ID } if reqJSON { // Decode JSON request decoder := json.NewDecoder(r.Body) err = decoder.Decode(&ss) if err != nil { log.Error("Couldn't parse new subscription JSON request: %v\n", err) return ErrBadJSON } } else { err = r.ParseForm() if err != nil { log.Error("Couldn't parse new subscription form request: %v\n", err) return ErrBadFormData } - err = app.formDecoder.Decode(&ss, r.PostForm) + decodedForm := sanitizeSubscriptionForm(r.PostForm) + err = app.formDecoder.Decode(&ss, decodedForm) if err != nil { log.Error("Continuing, but error decoding new subscription form request: %v\n", err) //return ErrBadFormData } } c, err := app.db.GetCollection(ss.CollAlias) if err != nil { log.Error("getCollection: %s", err) return err } c.hostName = app.cfg.App.Host from := c.CanonicalURL() isAuthorBanned, err := app.db.IsUserSilenced(c.OwnerID) if isAuthorBanned { log.Info("Author is silenced, so subscription is blocked.") return impart.HTTPError{http.StatusFound, from} } if ss.Web { if u != nil && u.ID == c.OwnerID { from = "/" + c.Alias + "/" } from += ss.Slug } if r.FormValue(spam.HoneypotFieldName()) != "" || r.FormValue("fake_password") != "" { log.Info("Honeypot field was filled out! Not subscribing.") return impart.HTTPError{http.StatusFound, from} } if ss.Email == "" && ss.UserID < 1 { log.Info("No subscriber data. Not subscribing.") return impart.HTTPError{http.StatusFound, from} } confirmed := app.db.IsSubscriberConfirmed(ss.Email) es, err := app.db.AddEmailSubscription(c.ID, ss.UserID, ss.Email, confirmed) if err != nil { log.Error("addEmailSubscription: %s", err) return err } // Send confirmation email if needed if !confirmed { err = sendSubConfirmEmail(app, c, ss.Email, es.ID, es.Token) if err != nil { log.Error("Failed to send subscription confirmation email: %s", err) return err } } if ss.Web { session, err := app.sessionStore.Get(r, userEmailCookieName) if err != nil { // The cookie should still save, even if there's an error. // Source: https://github.com/gorilla/sessions/issues/16#issuecomment-143642144 log.Error("Getting user email cookie: %v; ignoring", err) } if confirmed { addSessionFlash(app, w, r, "Subscribed. You'll now receive future blog posts via email.", nil) } else { addSessionFlash(app, w, r, "Please check your email and click the confirmation link to subscribe.", nil) } session.Values[userEmailCookieVal] = ss.Email err = session.Save(r, w) if err != nil { log.Error("save email cookie: %s", err) return err } return impart.HTTPError{http.StatusFound, from} } return impart.WriteSuccess(w, "", http.StatusAccepted) } func handleExportEmailSubscriptions(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) { vars := mux.Vars(r) var err error alias := vars["alias"] filename := "" u := getUserSession(app, r) if u == nil { return nil, filename, ErrNotLoggedIn } c, err := app.db.GetCollection(alias) if err != nil { return nil, filename, err } // Verify permissions / ownership if u.ID != c.OwnerID { return nil, filename, ErrForbiddenCollectionAccess } filename = "subscribers-" + alias + "-" + time.Now().Truncate(time.Second).UTC().Format("200601021504") subs, err := app.db.GetEmailSubscribers(c.ID, true) if err != nil { return nil, filename, err } var data []byte for _, sub := range subs { data = append(data, []byte(sub.Email.String+"\n")...) } data = bytes.TrimRight(data, "\n") return data, filename, err } func handleDeleteEmailSubscription(app *App, w http.ResponseWriter, r *http.Request) error { alias := collectionAliasFromReq(r) vars := mux.Vars(r) subID := vars["subscriber"] email := r.FormValue("email") token := r.FormValue("t") slug := r.FormValue("slug") isWeb := r.Method == "GET" // Display collection if this is a collection var c *Collection var err error if app.cfg.App.SingleUser { c, err = app.db.GetCollectionByID(1) } else { c, err = app.db.GetCollection(alias) } if err != nil { log.Error("Get collection: %s", err) return err } from := c.CanonicalURL() if subID != "" { // User unsubscribing via email, so assume action is taken by either current // user or not current user, and only use the request's information to // satisfy this unsubscribe, i.e. subscriberID and token. err = app.db.DeleteEmailSubscriber(subID, token) } else { // User unsubscribing through the web app, so assume action is taken by // currently-auth'd user. var userID int64 u := getUserSession(app, r) if u != nil { // User is logged in userID = u.ID if userID == c.OwnerID { from = "/" + c.Alias + "/" } } if email == "" && userID <= 0 { // Get email address from saved cookie session, err := app.sessionStore.Get(r, userEmailCookieName) if err != nil { log.Error("Unable to get email cookie: %s", err) } else { email = session.Values[userEmailCookieVal].(string) } } if email == "" && userID <= 0 { err = fmt.Errorf("No subscriber given.") log.Error("Not deleting subscription: %s", err) return err } err = app.db.DeleteEmailSubscriberByUser(email, userID, c.ID) } if err != nil { log.Error("Unable to delete subscriber: %v", err) return err } if isWeb { from += slug addSessionFlash(app, w, r, "Unsubscribed. You will no longer receive these blog posts via email.", nil) return impart.HTTPError{http.StatusFound, from} } return impart.WriteSuccess(w, "", http.StatusAccepted) } func handleConfirmEmailSubscription(app *App, w http.ResponseWriter, r *http.Request) error { alias := collectionAliasFromReq(r) subID := mux.Vars(r)["subscriber"] token := r.FormValue("t") var c *Collection var err error if app.cfg.App.SingleUser { c, err = app.db.GetCollectionByID(1) } else { c, err = app.db.GetCollection(alias) } if err != nil { log.Error("Get collection: %s", err) return err } from := c.CanonicalURL() err = app.db.UpdateSubscriberConfirmed(subID, token) if err != nil { addSessionFlash(app, w, r, err.Error(), nil) return impart.HTTPError{http.StatusFound, from} } addSessionFlash(app, w, r, "Confirmed! Thanks. Now you'll receive future blog posts via email.", nil) return impart.HTTPError{http.StatusFound, from} } func emailPost(app *App, p *PublicPost, collID int64) error { p.augmentContent() // Do some shortcode replacement. // Since the user is receiving this email, we can assume they're subscribed via email. - p.Content = strings.Replace(p.Content, shortCodeEmailSub, `

You're subscribed to email updates.

`, -1) + p.Content = strings.Replace(p.Content, "", `

You're subscribed to email updates.

`, -1) if p.HTMLContent == template.HTML("") { p.formatContent(app.cfg, false, false) } p.augmentReadingDestination() title := p.Title.String if title != "" { title = p.Title.String + "\n\n" } plainMsg := title + "A new post from " + p.CanonicalURL(app.cfg.App.Host) + "\n\n" + stripmd.Strip(p.Content) plainMsg += ` --------------------------------------------------------------------------------- Originally published on ` + p.Collection.DisplayTitle() + ` (` + p.Collection.CanonicalURL() + `), a blog you subscribe to. Sent to %recipient.to%. Unsubscribe: ` + p.Collection.CanonicalURL() + `email/unsubscribe/%recipient.id%?t=%recipient.token%` mlr, err := mailer.New(app.cfg.Email) if err != nil { return err } m, err := mlr.NewMessage(p.Collection.DisplayTitle()+" <"+p.Collection.Alias+"@"+app.cfg.Email.Domain+">", stripmd.Strip(p.DisplayTitle()), plainMsg) if err != nil { return err } replyTo := app.db.GetCollectionAttribute(collID, collAttrLetterReplyTo) if replyTo != "" { m.SetReplyTo(replyTo) } subs, err := app.db.GetEmailSubscribers(collID, true) if err != nil { log.Error("Unable to get email subscribers: %v", err) return err } if len(subs) == 0 { return nil } if title != "" { title = string(`

` + p.FormattedDisplayTitle() + `

`) } m.AddTag("New post") fontFam := "Lora, Palatino, Baskerville, serif" if p.IsSans() { fontFam = `"Open Sans", Tahoma, Arial, sans-serif` } else if p.IsMonospace() { fontFam = `Hack, consolas, Menlo-Regular, Menlo, Monaco, monospace, monospace` } // TODO: move this to a templated file and LESS-generated stylesheet fullHTML := `
` + title + `

From ` + p.DisplayCanonicalURL() + `

` + string(p.HTMLContent) + `

` // inline CSS html, err := inliner.Inline(fullHTML) if err != nil { log.Error("Unable to inline email HTML: %v", err) return err } m.SetHTML(html) log.Info("[email] Adding %d recipient(s)", len(subs)) for _, s := range subs { e := s.FinalEmail(app.keys) log.Info("[email] Adding %s", e) err = m.AddRecipientAndVariables(e, map[string]string{ "id": s.ID, "to": e, "token": s.Token, }) if err != nil { log.Error("Unable to add receipient %s: %s", e, err) } } err = mlr.Send(m) log.Info("[email] Email sent") if err != nil { log.Error("Unable to send post email: %v", err) return err } return nil } func sendSubConfirmEmail(app *App, c *Collection, email, subID, token string) error { if email == "" { return fmt.Errorf("You must supply an email to verify.") } // Send email mlr, err := mailer.New(app.cfg.Email) if err != nil { return err } plainMsg := "Confirm your subscription to " + c.DisplayTitle() + ` (` + c.CanonicalURL() + `) to start receiving future posts. Simply click the following link (or copy and paste it into your browser): ` + c.CanonicalURL() + "email/confirm/" + subID + "?t=" + token + ` If you didn't subscribe to this site or you're not sure why you're getting this email, you can delete it. You won't be subscribed or receive any future emails.` m, err := mlr.NewMessage(c.DisplayTitle()+" <"+c.Alias+"@"+app.cfg.Email.Domain+">", "Confirm your subscription to "+c.DisplayTitle(), plainMsg, fmt.Sprintf("<%s>", email)) if err != nil { return err } m.AddTag("Email Verification") m.SetHTML(`

Confirm your subscription to ` + c.DisplayTitle() + ` to start receiving future posts:

Subscribe to ` + c.DisplayTitle() + `

If you didn't subscribe to this site or you're not sure why you're getting this email, you can delete it. You won't be subscribed or receive any future emails.

`) err = mlr.Send(m) if err != nil { return err } return nil } diff --git a/email_test.go b/email_test.go new file mode 100644 index 0000000..87284ec --- /dev/null +++ b/email_test.go @@ -0,0 +1,63 @@ +package writefreely + +import ( + "net/url" + "testing" + + "github.com/writefreely/writefreely/spam" +) + +func TestSanitizeSubscriptionFormRemovesSpamFields(t *testing.T) { + honeypot := spam.HoneypotFieldName() + form := url.Values{ + "email": {"reader@example.com"}, + "web": {"1"}, + honeypot: {""}, + "fake_password": {"bot-value"}, + } + + clean := sanitizeSubscriptionForm(form) + + if clean.Get("email") != "reader@example.com" { + t.Fatalf("expected email to be preserved") + } + if clean.Get("web") != "1" { + t.Fatalf("expected web to be preserved") + } + if _, ok := clean[honeypot]; ok { + t.Fatalf("expected honeypot field to be removed") + } + if _, ok := clean["fake_password"]; ok { + t.Fatalf("expected fake_password field to be removed") + } + if _, ok := form[honeypot]; !ok { + t.Fatalf("expected original form honeypot field to remain") + } + if _, ok := form["fake_password"]; !ok { + t.Fatalf("expected original form fake_password field to remain") + } +} + +func TestSubscriptionFormDecodeAfterSanitize(t *testing.T) { + honeypot := spam.HoneypotFieldName() + form := url.Values{ + "email": {"reader@example.com"}, + "web": {"1"}, + honeypot: {""}, + "fake_password": {"bot-value"}, + } + + app := &App{} + app.InitDecoder() + + ss := SubmittedSubscription{} + if err := app.formDecoder.Decode(&ss, sanitizeSubscriptionForm(form)); err != nil { + t.Fatalf("expected decode to succeed, got %v", err) + } + if ss.Email != "reader@example.com" { + t.Fatalf("expected decoded email, got %q", ss.Email) + } + if !ss.Web { + t.Fatalf("expected decoded web flag to be true") + } +} diff --git a/handle.go b/handle.go index 1756be3..83c8a57 100644 --- a/handle.go +++ b/handle.go @@ -1,1013 +1,1015 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "fmt" "html/template" "net/http" "net/url" "runtime/debug" "strconv" "strings" "time" "github.com/gorilla/sessions" "github.com/writeas/impart" "github.com/writeas/web-core/log" "github.com/writefreely/go-gopher" "github.com/writefreely/writefreely/config" "github.com/writefreely/writefreely/page" ) // UserLevel represents the required user level for accessing an endpoint type UserLevel int const ( UserLevelNoneType UserLevel = iota // user or not -- ignored UserLevelOptionalType // user or not -- object fetched if user UserLevelNoneRequiredType // non-user (required) UserLevelUserType // user (required) ) func UserLevelNone(cfg *config.Config) UserLevel { return UserLevelNoneType } func UserLevelOptional(cfg *config.Config) UserLevel { return UserLevelOptionalType } func UserLevelNoneRequired(cfg *config.Config) UserLevel { return UserLevelNoneRequiredType } func UserLevelUser(cfg *config.Config) UserLevel { return UserLevelUserType } // UserLevelReader returns the permission level required for any route where // users can read published content. func UserLevelReader(cfg *config.Config) UserLevel { if cfg.App.Private { return UserLevelUserType } return UserLevelOptionalType } type ( handlerFunc func(app *App, w http.ResponseWriter, r *http.Request) error gopherFunc func(app *App, w gopher.ResponseWriter, r *gopher.Request) error userHandlerFunc func(app *App, u *User, w http.ResponseWriter, r *http.Request) error userApperHandlerFunc func(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error dataHandlerFunc func(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) authFunc func(app *App, r *http.Request) (*User, error) UserLevelFunc func(cfg *config.Config) UserLevel ) type Handler struct { errors *ErrorPages sessionStore sessions.Store app Apper } // ErrorPages hold template HTML error pages for displaying errors to the user. // In each, there should be a defined template named "base". type ErrorPages struct { NotFound *template.Template Gone *template.Template InternalServerError *template.Template UnavailableError *template.Template Blank *template.Template } // NewHandler returns a new Handler instance, using the given StaticPage data, // and saving alias to the application's CookieStore. func NewHandler(apper Apper) *Handler { h := &Handler{ errors: &ErrorPages{ NotFound: template.Must(template.New("").Parse("{{define \"base\"}}404

Not found.

{{end}}")), Gone: template.Must(template.New("").Parse("{{define \"base\"}}410

Gone.

{{end}}")), InternalServerError: template.Must(template.New("").Parse("{{define \"base\"}}500

Internal server error.

{{end}}")), UnavailableError: template.Must(template.New("").Parse("{{define \"base\"}}503

Service is temporarily unavailable.

{{end}}")), Blank: template.Must(template.New("").Parse("{{define \"base\"}}{{.Title}}

{{.Content}}

{{end}}")), }, sessionStore: apper.App().SessionStore(), app: apper, } return h } // NewWFHandler returns a new Handler instance, using WriteFreely template files. // You MUST call writefreely.InitTemplates() before this. func NewWFHandler(apper Apper) *Handler { h := NewHandler(apper) h.SetErrorPages(&ErrorPages{ NotFound: pages["404-general.tmpl"], Gone: pages["410.tmpl"], InternalServerError: pages["500.tmpl"], UnavailableError: pages["503.tmpl"], Blank: pages["blank.tmpl"], }) return h } // SetErrorPages sets the given set of ErrorPages as templates for any errors // that come up. func (h *Handler) SetErrorPages(e *ErrorPages) { h.errors = e } // User handles requests made in the web application by the authenticated user. // This provides user-friendly HTML pages and actions that work in the browser. func (h *Handler) User(f userHandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h.handleHTTPError(w, r, func() error { var status int start := time.Now() defer func() { if e := recover(); e != nil { log.Error("%s: %s", e, debug.Stack()) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = http.StatusInternalServerError } log.Info(h.app.ReqLog(r, status, time.Since(start))) }() u := getUserSession(h.app.App(), r) if u == nil { err := ErrNotLoggedIn status = err.Status return err } err := f(h.app.App(), u, w, r) if err == nil { status = http.StatusOK } else if impErr, ok := err.(impart.HTTPError); ok { status = impErr.Status if impErr == ErrUserNotFound { log.Info("Logged-in user not found. Logging out.") - sendRedirect(w, http.StatusFound, "/me/logout?to="+h.app.App().cfg.App.LandingPath()) + sendRedirect(w, http.StatusFound, h.app.App().cfg.App.PrefixPath("/me/logout?to="+h.app.App().cfg.App.LandingPath())) // Reset err so handleHTTPError does nothing err = nil } } else { status = http.StatusInternalServerError } return err }()) } } // Admin handles requests on /admin routes func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h.handleHTTPError(w, r, func() error { var status int start := time.Now() defer func() { if e := recover(); e != nil { log.Error("%s: %s", e, debug.Stack()) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = http.StatusInternalServerError } log.Info(h.app.ReqLog(r, status, time.Since(start))) }() u := getUserSession(h.app.App(), r) if u == nil || !u.IsAdmin() { err := impart.HTTPError{http.StatusNotFound, ""} status = err.Status return err } err := f(h.app.App(), u, w, r) if err == nil { status = http.StatusOK } else if err, ok := err.(impart.HTTPError); ok { status = err.Status } else { status = http.StatusInternalServerError } return err }()) } } // AdminApper handles requests on /admin routes that require an Apper. func (h *Handler) AdminApper(f userApperHandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h.handleHTTPError(w, r, func() error { var status int start := time.Now() defer func() { if e := recover(); e != nil { log.Error("%s: %s", e, debug.Stack()) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = http.StatusInternalServerError } log.Info(h.app.ReqLog(r, status, time.Since(start))) }() u := getUserSession(h.app.App(), r) if u == nil || !u.IsAdmin() { err := impart.HTTPError{http.StatusNotFound, ""} status = err.Status return err } err := f(h.app, u, w, r) if err == nil { status = http.StatusOK } else if err, ok := err.(impart.HTTPError); ok { status = err.Status } else { status = http.StatusInternalServerError } return err }()) } } func apiAuth(app *App, r *http.Request) (*User, error) { // Authorize user from Authorization header t := r.Header.Get("Authorization") if t == "" { return nil, ErrNoAccessToken } u := &User{ID: app.db.GetUserID(t)} if u.ID == -1 { return nil, ErrBadAccessToken } return u, nil } // optionalAPIAuth is used for endpoints that accept authenticated requests via // Authorization header or cookie, unlike apiAuth. It returns a different err // in the case where no Authorization header is present. func optionalAPIAuth(app *App, r *http.Request) (*User, error) { // Authorize user from Authorization header t := r.Header.Get("Authorization") if t == "" { return nil, ErrNotLoggedIn } u := &User{ID: app.db.GetUserID(t)} if u.ID == -1 { return nil, ErrBadAccessToken } return u, nil } func webAuth(app *App, r *http.Request) (*User, error) { u := getUserSession(app, r) if u == nil { return nil, ErrNotLoggedIn } return u, nil } // UserAPI handles requests made in the API by the authenticated user. // This provides user-friendly HTML pages and actions that work in the browser. func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc { return h.UserAll(false, f, apiAuth) } // UserWebAPI handles endpoints that accept a user authorized either via the web (cookies) or an Authorization header. func (h *Handler) UserWebAPI(f userHandlerFunc) http.HandlerFunc { return h.UserAll(false, f, func(app *App, r *http.Request) (*User, error) { // Authorize user via cookies u := getUserSession(app, r) if u != nil { return u, nil } // Fall back to access token, since user isn't logged in via web var err error u, err = apiAuth(app, r) if err != nil { return nil, err } return u, nil }) } func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { handleFunc := func() error { var status int start := time.Now() defer func() { if e := recover(); e != nil { log.Error("%s: %s", e, debug.Stack()) impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Something didn't work quite right."}) status = 500 } log.Info(h.app.ReqLog(r, status, time.Since(start))) }() u, err := a(h.app.App(), r) if err != nil { if err, ok := err.(impart.HTTPError); ok { status = err.Status } else { status = 500 } return err } err = f(h.app.App(), u, w, r) if err == nil { status = 200 } else if err, ok := err.(impart.HTTPError); ok { status = err.Status } else { status = 500 } return err } if web { h.handleHTTPError(w, r, handleFunc()) } else { h.handleError(w, r, handleFunc()) } } } func (h *Handler) RedirectOnErr(f handlerFunc, loc string) handlerFunc { return func(app *App, w http.ResponseWriter, r *http.Request) error { err := f(app, w, r) if err != nil { if ie, ok := err.(impart.HTTPError); ok { // Override default redirect with returned error's, if it's a // redirect error. if ie.Status == http.StatusFound { return ie } } return impart.HTTPError{http.StatusFound, loc} } return nil } } func (h *Handler) Page(n string) http.HandlerFunc { return h.Web(func(app *App, w http.ResponseWriter, r *http.Request) error { t, ok := pages[n] if !ok { return impart.HTTPError{http.StatusNotFound, "Page not found."} } sp := pageForReq(app, r) err := t.ExecuteTemplate(w, "base", sp) if err != nil { log.Error("Unable to render page: %v", err) } return err }, UserLevelOptional) } func (h *Handler) WebErrors(f handlerFunc, ul UserLevelFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // TODO: factor out this logic shared with Web() h.handleHTTPError(w, r, func() error { var status int start := time.Now() defer func() { if e := recover(); e != nil { u := getUserSession(h.app.App(), r) username := "None" if u != nil { username = u.Username } log.Error("User: %s\n\n%s: %s", username, e, debug.Stack()) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = 500 } log.Info(h.app.ReqLog(r, status, time.Since(start))) }() var session *sessions.Session var err error if ul(h.app.App().cfg) != UserLevelNoneType { session, err = h.sessionStore.Get(r, cookieName) if err != nil && (ul(h.app.App().cfg) == UserLevelNoneRequiredType || ul(h.app.App().cfg) == UserLevelUserType) { // Cookie is required, but we can ignore this error log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul(h.app.App().cfg), err) } _, gotUser := session.Values[cookieUserVal].(*User) if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser { - to := correctPageFromLoginAttempt(r) + to := correctPageFromLoginAttempt(h.app.App(), r) log.Info("Handler: Required NO user, but got one. Redirecting to %s", to) err := impart.HTTPError{http.StatusFound, to} status = err.Status return err } else if ul(h.app.App().cfg) == UserLevelUserType && !gotUser { log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.") err := ErrNotLoggedIn status = err.Status return err } } // TODO: pass User object to function err = f(h.app.App(), w, r) if err == nil { status = 200 } else if httpErr, ok := err.(impart.HTTPError); ok { status = httpErr.Status if status < 300 || status > 399 { addSessionFlash(h.app.App(), w, r, httpErr.Message, session) return impart.HTTPError{http.StatusFound, r.Referer()} } } else { e := fmt.Sprintf("[Web handler] 500: %v", err) if !strings.HasSuffix(e, "write: broken pipe") { log.Error(e) } else { log.Error(e) } log.Info("Web handler internal error render") h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = 500 } return err }()) } } func (h *Handler) CollectionPostOrStatic(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, ".") && !isRaw(r) { start := time.Now() status := 200 defer func() { log.Info(h.app.ReqLog(r, status, time.Since(start))) }() // Serve static file h.app.App().shttp.ServeHTTP(w, r) return } h.Web(viewCollectionPost, UserLevelReader)(w, r) } // Web handles requests made in the web application. This provides user- // friendly HTML pages and actions that work in the browser. func (h *Handler) Web(f handlerFunc, ul UserLevelFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h.handleHTTPError(w, r, func() error { var status int start := time.Now() defer func() { if e := recover(); e != nil { u := getUserSession(h.app.App(), r) username := "None" if u != nil { username = u.Username } log.Error("User: %s\n\n%s: %s", username, e, debug.Stack()) log.Info("Web deferred internal error render") h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = 500 } log.Info(h.app.ReqLog(r, status, time.Since(start))) }() if ul(h.app.App().cfg) != UserLevelNoneType { session, err := h.sessionStore.Get(r, cookieName) if err != nil && (ul(h.app.App().cfg) == UserLevelNoneRequiredType || ul(h.app.App().cfg) == UserLevelUserType) { // Cookie is required, but we can ignore this error log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul(h.app.App().cfg), err) } _, gotUser := session.Values[cookieUserVal].(*User) if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser { - to := correctPageFromLoginAttempt(r) + to := correctPageFromLoginAttempt(h.app.App(), r) log.Info("Handler: Required NO user, but got one. Redirecting to %s", to) err := impart.HTTPError{http.StatusFound, to} status = err.Status return err } else if ul(h.app.App().cfg) == UserLevelUserType && !gotUser { log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.") err := ErrNotLoggedIn status = err.Status return err } } // TODO: pass User object to function err := f(h.app.App(), w, r) if err == nil { status = 200 } else if httpErr, ok := err.(impart.HTTPError); ok { status = httpErr.Status } else { e := fmt.Sprintf("[Web handler] 500: %v", err) log.Error(e) log.Info("Web internal error render") h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = 500 } return err }()) } } func (h *Handler) All(f handlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h.handleError(w, r, func() error { // TODO: return correct "success" status status := 200 start := time.Now() defer func() { if e := recover(); e != nil { log.Error("%s:\n%s", e, debug.Stack()) impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Something didn't work quite right."}) status = 500 } log.Info(h.app.ReqLog(r, status, time.Since(start))) }() // TODO: do any needed authentication err := f(h.app.App(), w, r) if err != nil { if err, ok := err.(impart.HTTPError); ok { status = err.Status } else { status = 500 } } return err }()) } } func (h *Handler) PlainTextAPI(f handlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h.handleTextError(w, r, func() error { // TODO: return correct "success" status status := 200 start := time.Now() defer func() { if e := recover(); e != nil { log.Error("%s:\n%s", e, debug.Stack()) status = http.StatusInternalServerError w.WriteHeader(status) fmt.Fprintf(w, "Something didn't work quite right. The robots have alerted the humans.") } log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\" \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent(), r.Host)) }() err := f(h.app.App(), w, r) if err != nil { if err, ok := err.(impart.HTTPError); ok { status = err.Status } else { status = http.StatusInternalServerError } } return err }()) } } func (h *Handler) OAuth(f handlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h.handleOAuthError(w, r, func() error { // TODO: return correct "success" status status := 200 start := time.Now() defer func() { if e := recover(); e != nil { log.Error("%s:\n%s", e, debug.Stack()) impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Something didn't work quite right."}) status = 500 } log.Info(h.app.ReqLog(r, status, time.Since(start))) }() err := f(h.app.App(), w, r) if err != nil { if err, ok := err.(impart.HTTPError); ok { status = err.Status } else { status = 500 } } return err }()) } } func (h *Handler) AllReader(f handlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h.handleError(w, r, func() error { status := 200 start := time.Now() defer func() { if e := recover(); e != nil { log.Error("%s:\n%s", e, debug.Stack()) impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "Something didn't work quite right."}) status = 500 } log.Info(h.app.ReqLog(r, status, time.Since(start))) }() // Allow any origin, as public endpoints are handled in here w.Header().Set("Access-Control-Allow-Origin", "*") if h.app.App().cfg.App.Private { // This instance is private, so ensure it's being accessed by a valid user // Check if authenticated with an access token _, apiErr := optionalAPIAuth(h.app.App(), r) if apiErr != nil { if err, ok := apiErr.(impart.HTTPError); ok { status = err.Status } else { status = 500 } if apiErr == ErrNotLoggedIn { // Fall back to web auth since there was no access token given _, err := webAuth(h.app.App(), r) if err != nil { if err, ok := apiErr.(impart.HTTPError); ok { status = err.Status } else { status = 500 } return err } } else { return apiErr } } } err := f(h.app.App(), w, r) if err != nil { if err, ok := err.(impart.HTTPError); ok { status = err.Status } else { status = 500 } } return err }()) } } func (h *Handler) Download(f dataHandlerFunc, ul UserLevelFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h.handleHTTPError(w, r, func() error { var status int start := time.Now() defer func() { if e := recover(); e != nil { log.Error("%s: %s", e, debug.Stack()) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = 500 } log.Info(h.app.ReqLog(r, status, time.Since(start))) }() data, filename, err := f(h.app.App(), w, r) if err != nil { if err, ok := err.(impart.HTTPError); ok { status = err.Status } else { status = 500 } return err } ext := ".json" ct := "application/json" if strings.HasSuffix(r.URL.Path, ".csv") { ext = ".csv" ct = "text/csv" } else if strings.HasSuffix(r.URL.Path, ".zip") { ext = ".zip" ct = "application/zip" } w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s%s", filename, ext)) w.Header().Set("Content-Type", ct) w.Header().Set("Content-Length", strconv.Itoa(len(data))) fmt.Fprint(w, string(data)) status = 200 return nil }()) } } func (h *Handler) Redirect(url string, ul UserLevelFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h.handleHTTPError(w, r, func() error { start := time.Now() var status int if ul(h.app.App().cfg) != UserLevelNoneType { session, err := h.sessionStore.Get(r, cookieName) if err != nil && (ul(h.app.App().cfg) == UserLevelNoneRequiredType || ul(h.app.App().cfg) == UserLevelUserType) { // Cookie is required, but we can ignore this error log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul(h.app.App().cfg), err) } _, gotUser := session.Values[cookieUserVal].(*User) if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser { - to := correctPageFromLoginAttempt(r) + to := correctPageFromLoginAttempt(h.app.App(), r) log.Info("Handler: Required NO user, but got one. Redirecting to %s", to) err := impart.HTTPError{http.StatusFound, to} status = err.Status return err } else if ul(h.app.App().cfg) == UserLevelUserType && !gotUser { log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.") err := ErrNotLoggedIn status = err.Status return err } } - status = sendRedirect(w, http.StatusFound, url) + status = sendRedirect(w, http.StatusFound, h.app.App().cfg.App.PrefixPath(url)) log.Info(h.app.ReqLog(r, status, time.Since(start))) return nil }()) } } func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err error) { if err == nil { return } if err, ok := err.(impart.HTTPError); ok { if err.Status >= 300 && err.Status < 400 { - sendRedirect(w, err.Status, err.Message) + sendRedirect(w, err.Status, h.app.App().cfg.App.PrefixPath(err.Message)) return } else if err.Status == http.StatusUnauthorized { q := "" if r.URL.RawQuery != "" { q = url.QueryEscape("?" + r.URL.RawQuery) } - sendRedirect(w, http.StatusFound, "/login?to="+r.URL.Path+q) + sendRedirect(w, http.StatusFound, h.app.App().cfg.App.PrefixPath("/login?to="+r.URL.Path+q)) return } else if err.Status == http.StatusGone { w.WriteHeader(err.Status) p := &struct { page.StaticPage Content *template.HTML }{ StaticPage: pageForReq(h.app.App(), r), } if err.Message != "" { co := template.HTML(err.Message) p.Content = &co } h.errors.Gone.ExecuteTemplate(w, "base", p) return } else if err.Status == http.StatusNotFound { w.WriteHeader(err.Status) if IsActivityPubRequest(r) { // This is a fediverse request; simply return the header return } h.errors.NotFound.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) return } else if err.Status == http.StatusInternalServerError { w.WriteHeader(err.Status) log.Info("handleHTTPErorr internal error render") h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) return } else if err.Status == http.StatusServiceUnavailable { w.WriteHeader(err.Status) h.errors.UnavailableError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) return } else if err.Status == http.StatusAccepted { impart.WriteSuccess(w, "", err.Status) return } else { p := &struct { page.StaticPage Title string Content template.HTML }{ pageForReq(h.app.App(), r), fmt.Sprintf("Uh oh (%d)", err.Status), template.HTML(fmt.Sprintf("

%s

", err.Message)), } h.errors.Blank.ExecuteTemplate(w, "base", p) return } impart.WriteError(w, err) return } impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."}) } func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error) { if err == nil { return } if err, ok := err.(impart.HTTPError); ok { if err.Status >= 300 && err.Status < 400 { - sendRedirect(w, err.Status, err.Message) + sendRedirect(w, err.Status, h.app.App().cfg.App.PrefixPath(err.Message)) return } // if strings.Contains(r.Header.Get("Accept"), "text/html") { impart.WriteError(w, err) // } return } if IsJSON(r) { impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."}) return } h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) } func (h *Handler) handleTextError(w http.ResponseWriter, r *http.Request, err error) { if err == nil { return } if err, ok := err.(impart.HTTPError); ok { if err.Status >= 300 && err.Status < 400 { - sendRedirect(w, err.Status, err.Message) + sendRedirect(w, err.Status, h.app.App().cfg.App.PrefixPath(err.Message)) return } w.WriteHeader(err.Status) fmt.Fprintf(w, http.StatusText(err.Status)) return } w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "This is an unhelpful error message for a miscellaneous internal error.") } func (h *Handler) handleOAuthError(w http.ResponseWriter, r *http.Request, err error) { if err == nil { return } if err, ok := err.(impart.HTTPError); ok { if err.Status >= 300 && err.Status < 400 { - sendRedirect(w, err.Status, err.Message) + sendRedirect(w, err.Status, h.app.App().cfg.App.PrefixPath(err.Message)) return } impart.WriteOAuthError(w, err) return } impart.WriteOAuthError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."}) return } -func correctPageFromLoginAttempt(r *http.Request) string { +func correctPageFromLoginAttempt(app *App, r *http.Request) string { to := r.FormValue("to") if to == "" { - to = "/" + to = app.cfg.App.PrefixPath("/") } else if !strings.HasPrefix(to, "/") { - to = "/" + to + to = app.cfg.App.PrefixPath("/" + to) + } else { + to = app.cfg.App.PrefixPath(to) } return to } func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h.handleHTTPError(w, r, func() error { status := 200 start := time.Now() defer func() { if e := recover(); e != nil { log.Error("Handler.LogHandlerFunc\n\n%s: %s", e, debug.Stack()) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) status = 500 } // TODO: log actual status code returned log.Info(h.app.ReqLog(r, status, time.Since(start))) }() if h.app.App().cfg.App.Private { // This instance is private, so ensure it's being accessed by a valid user // Check if authenticated with an access token _, apiErr := optionalAPIAuth(h.app.App(), r) if apiErr != nil { if err, ok := apiErr.(impart.HTTPError); ok { status = err.Status } else { status = 500 } if apiErr == ErrNotLoggedIn { // Fall back to web auth since there was no access token given _, err := webAuth(h.app.App(), r) if err != nil { if err, ok := apiErr.(impart.HTTPError); ok { status = err.Status } else { status = 500 } return err } } else { return apiErr } } } f(w, r) return nil }()) } } func (h *Handler) Gopher(f gopherFunc) gopher.HandlerFunc { return func(w gopher.ResponseWriter, r *gopher.Request) { defer func() { if e := recover(); e != nil { log.Error("%s: %s", e, debug.Stack()) w.WriteError("An internal error occurred") } log.Info("gopher: %s", r.Selector) }() err := f(h.app.App(), w, r) if err != nil { log.Error("failed: %s", err) w.WriteError("the page failed for some reason (see logs)") } } } func sendRedirect(w http.ResponseWriter, code int, location string) int { w.Header().Set("Location", location) w.WriteHeader(code) return code } func cacheControl(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "public, max-age=604800, immutable") next.ServeHTTP(w, r) }) } diff --git a/hostmeta.go b/hostmeta.go index 9152bec..712b948 100644 --- a/hostmeta.go +++ b/hostmeta.go @@ -1,29 +1,31 @@ /* * Copyright © 2018-2019 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "fmt" "net/http" + + "github.com/writeas/go-webfinger" ) func handleViewHostMeta(app *App, w http.ResponseWriter, r *http.Request) error { w.Header().Set("Server", serverSoftware) w.Header().Set("Content-Type", "application/xrd+xml; charset=utf-8") meta := ` - + ` fmt.Fprintf(w, meta) return nil } diff --git a/jobs.go b/jobs.go index 02fcfeb..251b82d 100644 --- a/jobs.go +++ b/jobs.go @@ -1,73 +1,72 @@ package writefreely import ( - "time" - "github.com/writeas/web-core/log" + "time" ) type PostJob struct { ID int64 PostID string Action string Delay int64 } func addJob(app *App, p *PublicPost, action string, delay int64) error { j := &PostJob{ PostID: p.ID, Action: action, Delay: delay, } return app.db.InsertJob(j) } func startPublishJobsQueue(app *App) { t := time.NewTicker(62 * time.Second) for { log.Info("[jobs] Done.") <-t.C log.Info("[jobs] Fetching email publish jobs...") jobs, err := app.db.GetJobsToRun("email") if err != nil { log.Error("[jobs] %s - Skipping.", err) continue } log.Info("[jobs] Running %d email publish jobs...", len(jobs)) err = runJobs(app, jobs, true) if err != nil { log.Error("[jobs] Failed: %s", err) } } } func runJobs(app *App, jobs []*PostJob, reqColl bool) error { for _, j := range jobs { p, err := app.db.GetPost(j.PostID, 0) if err != nil { log.Info("[job #%d] Unable to get post: %s", j.ID, err) continue } if !p.CollectionID.Valid && reqColl { log.Info("[job #%d] Post %s not part of a collection", j.ID, p.ID) app.db.DeleteJob(j.ID) continue } coll, err := app.db.GetCollectionByID(p.CollectionID.Int64) if err != nil { log.Info("[job #%d] Unable to get collection: %s", j.ID, err) continue } coll.hostName = app.cfg.App.Host coll.ForPublic() p.Collection = &CollectionObj{Collection: *coll} err = emailPost(app, p, p.Collection.ID) if err != nil { log.Error("[job #%d] Failed to email post %s", j.ID, p.ID) continue } log.Info("[job #%d] Success for post %s.", j.ID, p.ID) app.db.DeleteJob(j.ID) } return nil } diff --git a/keys.go b/keys.go index fa2adb5..b5896f7 100644 --- a/keys.go +++ b/keys.go @@ -1,75 +1,74 @@ /* * Copyright © 2018-2019, 2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( - "os" - "path/filepath" - "github.com/writeas/web-core/log" "github.com/writefreely/writefreely/key" + "os" + "path/filepath" ) const ( keysDir = "keys" ) var ( emailKeyPath = filepath.Join(keysDir, "email.aes256") cookieAuthKeyPath = filepath.Join(keysDir, "cookies_auth.aes256") cookieKeyPath = filepath.Join(keysDir, "cookies_enc.aes256") csrfKeyPath = filepath.Join(keysDir, "csrf.aes256") ) // InitKeys loads encryption keys into memory via the given Apper interface func InitKeys(apper Apper) error { log.Info("Loading encryption keys...") err := apper.LoadKeys() if err != nil { return err } return nil } func initKeyPaths(app *App) { emailKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, emailKeyPath) cookieAuthKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieAuthKeyPath) cookieKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieKeyPath) csrfKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, csrfKeyPath) } // generateKey generates a key at the given path used for the encryption of // certain user data. Because user data becomes unrecoverable without these // keys, this won't overwrite any existing key, and instead outputs a message. func generateKey(path string) error { // Check if key file exists if _, err := os.Stat(path); err == nil { log.Info("%s already exists. rm the file if you understand the consequences.", path) return nil } else if !os.IsNotExist(err) { log.Error("%s", err) return err } log.Info("Generating %s.", path) b, err := key.GenerateBytes(key.EncKeysBytes) if err != nil { log.Error("FAILED. %s. Run writefreely --gen-keys again.", err) return err } err = os.WriteFile(path, b, 0600) if err != nil { log.Error("FAILED writing file: %s", err) return err } log.Info("Success.") return nil } diff --git a/less/icons.less b/less/icons.less index 2be5064..b704077 100644 --- a/less/icons.less +++ b/less/icons.less @@ -1,38 +1,65 @@ @font-face { font-family: 'Material Icons'; font-style: normal; font-weight: 400; - src: url(MaterialIcons-Regular.eot); /* For IE6-8 */ + src: url(../fonts/MaterialIcons-Regular.eot); /* For IE6-8 */ src: local('Material Icons'), local('MaterialIcons-Regular'), - url(/fonts/MaterialIcons-Regular.woff2) format('woff2'), - url(/fonts/MaterialIcons-Regular.woff) format('woff'), - url(/fonts/MaterialIcons-Regular.ttf) format('truetype'); + url(../fonts/MaterialIcons-Regular.woff2) format('woff2'), + url(../fonts/MaterialIcons-Regular.woff) format('woff'), + url(../fonts/MaterialIcons-Regular.ttf) format('truetype'); } .material-icons { font-family: 'Material Icons'; font-weight: normal; font-style: normal; font-size: 24px; /* Preferred icon size */ display: inline-block; width: 1em; height: 1em; line-height: 1; text-transform: none; letter-spacing: normal; word-wrap: normal; white-space: nowrap; direction: ltr; + margin-right: 0.5em; + vertical-align: middle; /* Support for all WebKit browsers. */ -webkit-font-smoothing: antialiased; /* Support for Safari and Chrome. */ text-rendering: optimizeLegibility; /* Support for Firefox. */ -moz-osx-font-smoothing: grayscale; /* Support for IE. */ font-feature-settings: 'liga'; } + +/* Icon size variants */ +.material-icons.md-18 { + font-size: 18px; + width: 18px; + height: 18px; +} + +.material-icons.md-24 { + font-size: 24px; + width: 24px; + height: 24px; +} + +.material-icons.md-36 { + font-size: 36px; + width: 36px; + height: 36px; +} + +.material-icons.md-48 { + font-size: 48px; + width: 48px; + height: 48px; +} diff --git a/mailer/mailer.go b/mailer/mailer.go index a08643c..d017ee6 100644 --- a/mailer/mailer.go +++ b/mailer/mailer.go @@ -1,182 +1,184 @@ /* * Copyright © 2024 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package mailer import ( "fmt" "strings" "github.com/mailgun/mailgun-go" "github.com/writeas/web-core/log" "github.com/writefreely/writefreely/config" mail "github.com/xhit/go-simple-mail/v2" ) type ( // Mailer holds configurations for the preferred mailing provider. Mailer struct { smtp *mail.SMTPServer mailGun *mailgun.MailgunImpl } // Message holds the email contents and metadata for the preferred mailing provider. Message struct { mgMsg *mailgun.Message smtpMsg *SmtpMessage } SmtpMessage struct { from string replyTo string subject string recipients []Recipient html string text string } Recipient struct { email string vars map[string]string } ) // New creates a new Mailer from the instance's config.EmailCfg, returning an error if not properly configured. func New(eCfg config.EmailCfg) (*Mailer, error) { m := &Mailer{} if eCfg.Domain != "" && eCfg.MailgunPrivate != "" { m.mailGun = mailgun.NewMailgun(eCfg.Domain, eCfg.MailgunPrivate) if eCfg.MailgunEurope { m.mailGun.SetAPIBase("https://api.eu.mailgun.net/v3") } } else if eCfg.Username != "" && eCfg.Password != "" && eCfg.Host != "" && eCfg.Port > 0 { m.smtp = mail.NewSMTPClient() m.smtp.Host = eCfg.Host m.smtp.Port = eCfg.Port m.smtp.Username = eCfg.Username m.smtp.Password = eCfg.Password - if eCfg.EnableStartTLS { + if eCfg.EnableSSL { + m.smtp.Encryption = mail.EncryptionSSLTLS + } else if eCfg.EnableStartTLS { m.smtp.Encryption = mail.EncryptionSTARTTLS } // To allow sending multiple email m.smtp.KeepAlive = true } else { return nil, fmt.Errorf("no email provider is configured") } return m, nil } // NewMessage creates a new Message from the given parameters. func (m *Mailer) NewMessage(from, subject, text string, to ...string) (*Message, error) { msg := &Message{} if m.mailGun != nil { msg.mgMsg = m.mailGun.NewMessage(from, subject, text, to...) } else if m.smtp != nil { msg.smtpMsg = &SmtpMessage{ from: from, replyTo: "", subject: subject, - recipients: make([]Recipient, len(to)), + recipients: make([]Recipient, 0, len(to)), html: "", text: text, } for _, r := range to { msg.smtpMsg.recipients = append(msg.smtpMsg.recipients, Recipient{r, make(map[string]string)}) } } return msg, nil } // SetHTML sets the body of the message. func (m *Message) SetHTML(html string) { if m.smtpMsg != nil { m.smtpMsg.html = html } else if m.mgMsg != nil { m.mgMsg.SetHtml(html) } } func (m *Message) SetReplyTo(replyTo string) { if m.smtpMsg != nil { m.smtpMsg.replyTo = replyTo } else { m.mgMsg.SetReplyTo(replyTo) } } // AddTag attaches a tag to the Message for providers that support it. func (m *Message) AddTag(tag string) { if m.mgMsg != nil { m.mgMsg.AddTag(tag) } } func (m *Message) AddRecipientAndVariables(r string, vars map[string]string) error { if m.smtpMsg != nil { m.smtpMsg.recipients = append(m.smtpMsg.recipients, Recipient{r, vars}) return nil } else { varsInterfaces := make(map[string]interface{}, len(vars)) for k, v := range vars { varsInterfaces[k] = v } return m.mgMsg.AddRecipientAndVariables(r, varsInterfaces) } } // Send sends the given message via the preferred provider. func (m *Mailer) Send(msg *Message) error { if m.smtp != nil { client, err := m.smtp.Connect() if err != nil { return err } emailSent := false for _, r := range msg.smtpMsg.recipients { customMsg := mail.NewMSG() customMsg.SetFrom(msg.smtpMsg.from) if msg.smtpMsg.replyTo != "" { customMsg.SetReplyTo(msg.smtpMsg.replyTo) } customMsg.SetSubject(msg.smtpMsg.subject) customMsg.AddTo(r.email) cText := msg.smtpMsg.text cHtml := msg.smtpMsg.html for v, value := range r.vars { placeHolder := fmt.Sprintf("%%recipient.%s%%", v) cText = strings.ReplaceAll(cText, placeHolder, value) cHtml = strings.ReplaceAll(cHtml, placeHolder, value) } customMsg.SetBody(mail.TextHTML, cHtml) customMsg.AddAlternative(mail.TextPlain, cText) e := customMsg.Error if e == nil { e = customMsg.Send(client) } if e == nil { emailSent = true } else { log.Error("Unable to send email to %s: %v", r.email, e) err = e } } if !emailSent { // only send an error if no email could be sent (to avoid retry of successfully sent emails) return err } } else if m.mailGun != nil { _, _, err := m.mailGun.Send(msg.mgMsg) if err != nil { return err } } return nil } diff --git a/main_test.go b/main_test.go index 4d28143..9db7a7e 100644 --- a/main_test.go +++ b/main_test.go @@ -1,154 +1,153 @@ package writefreely import ( "context" "database/sql" "encoding/gob" "errors" "fmt" + uuid "github.com/nu7hatch/gouuid" + "github.com/stretchr/testify/assert" "math/rand" "os" "strings" "testing" "time" - - uuid "github.com/nu7hatch/gouuid" - "github.com/stretchr/testify/assert" ) var testDB *sql.DB type ScopedTestBody func(*sql.DB) // TestMain provides testing infrastructure within this package. func TestMain(m *testing.M) { rand.Seed(time.Now().UTC().UnixNano()) gob.Register(&User{}) if runMySQLTests() { var err error testDB, err = initMySQL(os.Getenv("WF_USER"), os.Getenv("WF_PASSWORD"), os.Getenv("WF_DB"), os.Getenv("WF_HOST")) if err != nil { fmt.Println(err) return } } code := m.Run() if runMySQLTests() { if closeErr := testDB.Close(); closeErr != nil { fmt.Println(closeErr) } } os.Exit(code) } func runMySQLTests() bool { return len(os.Getenv("TEST_MYSQL")) > 0 } func initMySQL(dbUser, dbPassword, dbName, dbHost string) (*sql.DB, error) { if dbUser == "" || dbPassword == "" { return nil, errors.New("database user or password not set") } if dbHost == "" { dbHost = "localhost" } if dbName == "" { dbName = "writefreely" } dsn := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?charset=utf8mb4&parseTime=true", dbUser, dbPassword, dbHost, dbName) db, err := sql.Open("mysql", dsn) if err != nil { return nil, err } if err := ensureMySQL(db); err != nil { return nil, err } return db, nil } func ensureMySQL(db *sql.DB) error { if err := db.Ping(); err != nil { return err } db.SetMaxOpenConns(250) return nil } // withTestDB provides a scoped database connection. func withTestDB(t *testing.T, testBody ScopedTestBody) { db, cleanup, err := newTestDatabase(testDB, os.Getenv("WF_USER"), os.Getenv("WF_PASSWORD"), os.Getenv("WF_DB"), os.Getenv("WF_HOST"), ) assert.NoError(t, err) defer func() { assert.NoError(t, cleanup()) }() testBody(db) } // newTestDatabase creates a new temporary test database. When a test // database connection is returned, it will have created a new database and // initialized it with tables from a reference database. func newTestDatabase(base *sql.DB, dbUser, dbPassword, dbName, dbHost string) (*sql.DB, func() error, error) { var err error var baseName = dbName if baseName == "" { row := base.QueryRow("SELECT DATABASE()") err := row.Scan(&baseName) if err != nil { return nil, nil, err } } tUUID, _ := uuid.NewV4() suffix := strings.Replace(tUUID.String(), "-", "_", -1) newDBName := baseName + suffix _, err = base.Exec("CREATE DATABASE " + newDBName) if err != nil { return nil, nil, err } newDB, err := initMySQL(dbUser, dbPassword, newDBName, dbHost) if err != nil { return nil, nil, err } rows, err := base.Query("SHOW TABLES IN " + baseName) if err != nil { return nil, nil, err } for rows.Next() { var tableName string if err := rows.Scan(&tableName); err != nil { return nil, nil, err } query := fmt.Sprintf("CREATE TABLE %s LIKE %s.%s", tableName, baseName, tableName) if _, err := newDB.Exec(query); err != nil { return nil, nil, err } } cleanup := func() error { if closeErr := newDB.Close(); closeErr != nil { fmt.Println(closeErr) } _, err = base.Exec("DROP DATABASE " + newDBName) return err } return newDB, cleanup, nil } func countRows(t *testing.T, ctx context.Context, db *sql.DB, count int, query string, args ...interface{}) { var returned int err := db.QueryRowContext(ctx, query, args...).Scan(&returned) assert.NoError(t, err, "error executing query %s and args %s", query, args) assert.Equal(t, count, returned, "unexpected return count %d, expected %d from %s and args %s", returned, count, query, args) } diff --git a/monetization.go b/monetization.go index 05e81db..4d6b42b 100644 --- a/monetization.go +++ b/monetization.go @@ -1,161 +1,160 @@ /* * Copyright © 2020-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "bytes" "fmt" + "github.com/gorilla/mux" + "github.com/writeas/impart" + "github.com/writeas/web-core/log" "io" "net/http" "net/url" "os" "strings" - - "github.com/gorilla/mux" - "github.com/writeas/impart" - "github.com/writeas/web-core/log" ) func displayMonetization(monetization, alias string) string { if monetization == "" { return "" } ptrURL, err := url.Parse(strings.Replace(monetization, "$", "https://", 1)) if err == nil { if strings.HasSuffix(ptrURL.Host, ".xrptipbot.com") { // xrp tip bot doesn't support stream receipts, so return plain pointer return monetization } } u := os.Getenv("PAYMENT_HOST") if u == "" { return "$webmonetization.org/api/receipts/" + url.PathEscape(monetization) } u += "/" + alias return u } func handleSPSPEndpoint(app *App, w http.ResponseWriter, r *http.Request) error { idStr := r.FormValue("id") id, err := url.QueryUnescape(idStr) if err != nil { log.Error("Unable to unescape: %s", err) return err } var c *Collection if strings.IndexRune(id, '.') > 0 && app.cfg.App.SingleUser { c, err = app.db.GetCollectionByID(1) } else { c, err = app.db.GetCollection(id) } if err != nil { return err } pointer := c.Monetization if pointer == "" { err := impart.HTTPError{http.StatusNotFound, "No monetization pointer."} return err } fmt.Fprintf(w, pointer) return nil } func handleGetSplitContent(app *App, w http.ResponseWriter, r *http.Request) error { var collID int64 var collLookupID string var coll *Collection var err error vars := mux.Vars(r) if collAlias := vars["alias"]; collAlias != "" { // Fetch collection information, since an alias is provided coll, err = app.db.GetCollection(collAlias) if err != nil { return err } collID = coll.ID collLookupID = coll.Alias } p, err := app.db.GetPost(vars["post"], collID) if err != nil { return err } receipt := r.FormValue("receipt") if receipt == "" { return impart.HTTPError{http.StatusBadRequest, "No `receipt` given."} } err = verifyReceipt(receipt, collLookupID) if err != nil { return err } d := struct { Content string `json:"body"` HTMLContent string `json:"html_body"` }{} if exc := strings.Index(p.Content, shortCodePaid); exc > -1 { baseURL := "" if coll != nil { baseURL = coll.CanonicalURL() } d.Content = p.Content[exc+len(shortCodePaid):] d.HTMLContent = applyMarkdown([]byte(d.Content), baseURL, app.cfg) } return impart.WriteSuccess(w, d, http.StatusOK) } func verifyReceipt(receipt, id string) error { receiptsHost := os.Getenv("RECEIPTS_HOST") if receiptsHost == "" { receiptsHost = "https://webmonetization.org/api/receipts/verify?id=" + id } else { receiptsHost = fmt.Sprintf("%s/receipts?id=%s", receiptsHost, id) } log.Info("Verifying receipt %s at %s", receipt, receiptsHost) r, err := http.NewRequest("POST", receiptsHost, bytes.NewBufferString(receipt)) if err != nil { log.Error("Unable to create new request to %s: %s", receiptsHost, err) return err } resp, err := http.DefaultClient.Do(r) if err != nil { log.Error("Unable to Do() request to %s: %s", receiptsHost, err) return err } if resp != nil && resp.Body != nil { defer resp.Body.Close() } body, err := io.ReadAll(resp.Body) if err != nil { log.Error("Unable to read %s response body: %s", receiptsHost, err) return err } log.Info("Status : %s", resp.Status) log.Info("Response: %s", body) if resp.StatusCode != http.StatusOK { log.Error("Bad response from %s:\nStatus: %d\n%s", receiptsHost, resp.StatusCode, string(body)) return impart.HTTPError{resp.StatusCode, string(body)} } return nil } diff --git a/nodeinfo.go b/nodeinfo.go index 608aa74..a2bb349 100644 --- a/nodeinfo.go +++ b/nodeinfo.go @@ -1,122 +1,122 @@ /* * Copyright © 2018-2019, 2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "strings" "github.com/writeas/web-core/log" "github.com/writefreely/go-nodeinfo" "github.com/writefreely/writefreely/config" ) type nodeInfoResolver struct { cfg *config.Config db *datastore } func nodeInfoConfig(db *datastore, cfg *config.Config) *nodeinfo.Config { name := cfg.App.SiteName desc := cfg.App.SiteDesc if desc == "" { desc = "Minimal, federated blogging platform." } if cfg.App.SingleUser { // Fetch blog information, instead coll, err := db.GetCollectionByID(1) if err == nil { desc = coll.Description } } return &nodeinfo.Config{ - BaseURL: cfg.App.Host, + BaseURL: cfg.App.AbsoluteHost(), InfoURL: "/api/nodeinfo", Metadata: nodeinfo.Metadata{ NodeName: name, NodeDescription: desc, Private: cfg.App.Private, Software: nodeinfo.SoftwareMeta{ HomePage: softwareURL, GitHub: "https://github.com/writefreely/writefreely", Follow: "https://writing.exchange/@writefreely", }, MaxBlogs: cfg.App.MaxBlogs, PublicReader: cfg.App.LocalTimeline, Invites: cfg.App.UserInvites != "", }, Protocols: []nodeinfo.NodeProtocol{ nodeinfo.ProtocolActivityPub, }, Services: nodeinfo.Services{ Inbound: []nodeinfo.NodeService{}, Outbound: []nodeinfo.NodeService{ nodeinfo.ServiceRSS, }, }, Software: nodeinfo.SoftwareInfo{ Name: strings.ToLower(serverSoftware), Version: softwareVer, }, } } func (r nodeInfoResolver) IsOpenRegistration() (bool, error) { return r.cfg.App.OpenRegistration, nil } func (r nodeInfoResolver) Usage() (nodeinfo.Usage, error) { var collCount, postCount int64 var activeHalfYear, activeMonth int var err error collCount, err = r.db.GetTotalCollections() if err != nil { collCount = 0 } postCount, err = r.db.GetTotalPosts() if err != nil { log.Error("Unable to fetch post counts: %v", err) } if r.cfg.App.PublicStats { // Display bi-yearly / monthly stats err = r.db.QueryRow(`SELECT COUNT(*) FROM ( SELECT DISTINCT collection_id FROM posts INNER JOIN collections c ON collection_id = c.id WHERE collection_id IS NOT NULL AND updated > DATE_SUB(NOW(), INTERVAL 6 MONTH)) co`).Scan(&activeHalfYear) if err != nil { log.Error("Failed getting 6-month active user stats: %s", err) } err = r.db.QueryRow(`SELECT COUNT(*) FROM ( SELECT DISTINCT collection_id FROM posts INNER JOIN collections c ON collection_id = c.id WHERE collection_id IS NOT NULL AND updated > DATE_SUB(NOW(), INTERVAL 1 MONTH)) co`).Scan(&activeMonth) if err != nil { log.Error("Failed getting 1-month active user stats: %s", err) } } return nodeinfo.Usage{ Users: nodeinfo.UsageUsers{ Total: int(collCount), ActiveHalfYear: activeHalfYear, ActiveMonth: activeMonth, }, LocalPosts: int(postCount), }, nil } diff --git a/oauth.go b/oauth.go index 06a2d20..9cd774b 100644 --- a/oauth.go +++ b/oauth.go @@ -1,472 +1,472 @@ /* * Copyright © 2019-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/writeas/impart" "github.com/writeas/web-core/log" "github.com/writefreely/writefreely/config" ) // OAuthButtons holds display information for different OAuth providers we support. type OAuthButtons struct { SlackEnabled bool WriteAsEnabled bool GitLabEnabled bool GitLabDisplayName string GiteaEnabled bool GiteaDisplayName string GenericEnabled bool GenericDisplayName string } // NewOAuthButtons creates a new OAuthButtons struct based on our app configuration. func NewOAuthButtons(cfg *config.Config) *OAuthButtons { return &OAuthButtons{ SlackEnabled: cfg.SlackOauth.ClientID != "", WriteAsEnabled: cfg.WriteAsOauth.ClientID != "", GitLabEnabled: cfg.GitlabOauth.ClientID != "", GitLabDisplayName: config.OrDefaultString(cfg.GitlabOauth.DisplayName, gitlabDisplayName), GiteaEnabled: cfg.GiteaOauth.ClientID != "", GiteaDisplayName: config.OrDefaultString(cfg.GiteaOauth.DisplayName, giteaDisplayName), GenericEnabled: cfg.GenericOauth.ClientID != "", GenericDisplayName: config.OrDefaultString(cfg.GenericOauth.DisplayName, genericOauthDisplayName), } } // TokenResponse contains data returned when a token is created either // through a code exchange or using a refresh token. type TokenResponse struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` RefreshToken string `json:"refresh_token"` TokenType string `json:"token_type"` Error string `json:"error"` } // InspectResponse contains data returned when an access token is inspected. type InspectResponse struct { ClientID string `json:"client_id"` UserID string `json:"user_id"` ExpiresAt time.Time `json:"expires_at"` Username string `json:"username"` DisplayName string `json:"-"` Email string `json:"email"` Error string `json:"error"` } // tokenRequestMaxLen is the most bytes that we'll read from the /oauth/token // endpoint. One megabyte is plenty. const tokenRequestMaxLen = 1000000 // infoRequestMaxLen is the most bytes that we'll read from the // /oauth/inspect endpoint. const infoRequestMaxLen = 1000000 // OAuthDatastoreProvider provides a minimal interface of data store, config, // and session store for use with the oauth handlers. type OAuthDatastoreProvider interface { DB() OAuthDatastore Config() *config.Config SessionStore() sessions.Store } // OAuthDatastore provides a minimal interface of data store methods used in // oauth functionality. type OAuthDatastore interface { GetIDForRemoteUser(context.Context, string, string, string) (int64, error) RecordRemoteUserID(context.Context, int64, string, string, string, string) error ValidateOAuthState(context.Context, string) (string, string, int64, string, error) GenerateOAuthState(context.Context, string, string, int64, string) (string, error) CreateUser(*config.Config, *User, string, string) error GetUserByID(int64) (*User, error) } type HttpClient interface { Do(req *http.Request) (*http.Response, error) } type oauthClient interface { GetProvider() string GetClientID() string GetCallbackLocation() string buildLoginURL(state string) (string, error) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) } type callbackProxyClient struct { server string callbackLocation string httpClient HttpClient } type oauthHandler struct { Config *config.Config DB OAuthDatastore Store sessions.Store EmailKey []byte oauthClient oauthClient callbackProxy *callbackProxyClient } func (h oauthHandler) viewOauthInit(app *App, w http.ResponseWriter, r *http.Request) error { ctx := r.Context() var attachUser int64 if attach := r.URL.Query().Get("attach"); attach == "t" { user, _ := getUserAndSession(app, r) if user == nil { return impart.HTTPError{http.StatusInternalServerError, "cannot attach auth to user: user not found in session"} } attachUser = user.ID } state, err := h.DB.GenerateOAuthState(ctx, h.oauthClient.GetProvider(), h.oauthClient.GetClientID(), attachUser, r.FormValue("invite_code")) if err != nil { log.Error("viewOauthInit error: %s", err) return impart.HTTPError{http.StatusInternalServerError, "could not prepare oauth redirect url"} } if h.callbackProxy != nil { if err := h.callbackProxy.register(ctx, state); err != nil { log.Error("viewOauthInit error: %s", err) return impart.HTTPError{http.StatusInternalServerError, "could not register state server"} } } location, err := h.oauthClient.buildLoginURL(state) if err != nil { log.Error("viewOauthInit error: %s", err) return impart.HTTPError{http.StatusInternalServerError, "could not prepare oauth redirect url"} } return impart.HTTPError{http.StatusTemporaryRedirect, location} } func configureSlackOauth(parentHandler *Handler, r *mux.Router, app *App) { if app.Config().SlackOauth.ClientID != "" { - callbackLocation := app.Config().App.Host + "/oauth/callback/slack" + callbackLocation := app.Config().App.AbsoluteURL("/oauth/callback/slack") var stateRegisterClient *callbackProxyClient = nil if app.Config().SlackOauth.CallbackProxyAPI != "" { stateRegisterClient = &callbackProxyClient{ server: app.Config().SlackOauth.CallbackProxyAPI, - callbackLocation: app.Config().App.Host + "/oauth/callback/slack", + callbackLocation: app.Config().App.AbsoluteURL("/oauth/callback/slack"), httpClient: config.DefaultHTTPClient(), } callbackLocation = app.Config().SlackOauth.CallbackProxy } oauthClient := slackOauthClient{ ClientID: app.Config().SlackOauth.ClientID, ClientSecret: app.Config().SlackOauth.ClientSecret, TeamID: app.Config().SlackOauth.TeamID, HttpClient: config.DefaultHTTPClient(), CallbackLocation: callbackLocation, } configureOauthRoutes(parentHandler, r, app, oauthClient, stateRegisterClient) } } func configureWriteAsOauth(parentHandler *Handler, r *mux.Router, app *App) { if app.Config().WriteAsOauth.ClientID != "" { - callbackLocation := app.Config().App.Host + "/oauth/callback/write.as" + callbackLocation := app.Config().App.AbsoluteURL("/oauth/callback/write.as") var callbackProxy *callbackProxyClient = nil if app.Config().WriteAsOauth.CallbackProxy != "" { callbackProxy = &callbackProxyClient{ server: app.Config().WriteAsOauth.CallbackProxyAPI, - callbackLocation: app.Config().App.Host + "/oauth/callback/write.as", + callbackLocation: app.Config().App.AbsoluteURL("/oauth/callback/write.as"), httpClient: config.DefaultHTTPClient(), } callbackLocation = app.Config().WriteAsOauth.CallbackProxy } oauthClient := writeAsOauthClient{ ClientID: app.Config().WriteAsOauth.ClientID, ClientSecret: app.Config().WriteAsOauth.ClientSecret, ExchangeLocation: config.OrDefaultString(app.Config().WriteAsOauth.TokenLocation, writeAsExchangeLocation), InspectLocation: config.OrDefaultString(app.Config().WriteAsOauth.InspectLocation, writeAsIdentityLocation), AuthLocation: config.OrDefaultString(app.Config().WriteAsOauth.AuthLocation, writeAsAuthLocation), HttpClient: config.DefaultHTTPClient(), CallbackLocation: callbackLocation, } configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy) } } func configureGitlabOauth(parentHandler *Handler, r *mux.Router, app *App) { if app.Config().GitlabOauth.ClientID != "" { - callbackLocation := app.Config().App.Host + "/oauth/callback/gitlab" + callbackLocation := app.Config().App.AbsoluteURL("/oauth/callback/gitlab") var callbackProxy *callbackProxyClient = nil if app.Config().GitlabOauth.CallbackProxy != "" { callbackProxy = &callbackProxyClient{ server: app.Config().GitlabOauth.CallbackProxyAPI, - callbackLocation: app.Config().App.Host + "/oauth/callback/gitlab", + callbackLocation: app.Config().App.AbsoluteURL("/oauth/callback/gitlab"), httpClient: config.DefaultHTTPClient(), } callbackLocation = app.Config().GitlabOauth.CallbackProxy } address := config.OrDefaultString(app.Config().GitlabOauth.Host, gitlabHost) oauthClient := gitlabOauthClient{ ClientID: app.Config().GitlabOauth.ClientID, ClientSecret: app.Config().GitlabOauth.ClientSecret, ExchangeLocation: address + "/oauth/token", InspectLocation: address + "/api/v4/user", AuthLocation: address + "/oauth/authorize", HttpClient: config.DefaultHTTPClient(), CallbackLocation: callbackLocation, } configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy) } } func configureGenericOauth(parentHandler *Handler, r *mux.Router, app *App) { if app.Config().GenericOauth.ClientID != "" { - callbackLocation := app.Config().App.Host + "/oauth/callback/generic" + callbackLocation := app.Config().App.AbsoluteURL("/oauth/callback/generic") var callbackProxy *callbackProxyClient = nil if app.Config().GenericOauth.CallbackProxy != "" { callbackProxy = &callbackProxyClient{ server: app.Config().GenericOauth.CallbackProxyAPI, - callbackLocation: app.Config().App.Host + "/oauth/callback/generic", + callbackLocation: app.Config().App.AbsoluteURL("/oauth/callback/generic"), httpClient: config.DefaultHTTPClient(), } callbackLocation = app.Config().GenericOauth.CallbackProxy } oauthClient := genericOauthClient{ ClientID: app.Config().GenericOauth.ClientID, ClientSecret: app.Config().GenericOauth.ClientSecret, ExchangeLocation: app.Config().GenericOauth.Host + app.Config().GenericOauth.TokenEndpoint, InspectLocation: app.Config().GenericOauth.Host + app.Config().GenericOauth.InspectEndpoint, AuthLocation: app.Config().GenericOauth.Host + app.Config().GenericOauth.AuthEndpoint, HttpClient: config.DefaultHTTPClient(), CallbackLocation: callbackLocation, Scope: config.OrDefaultString(app.Config().GenericOauth.Scope, "read_user"), MapUserID: config.OrDefaultString(app.Config().GenericOauth.MapUserID, "user_id"), MapUsername: config.OrDefaultString(app.Config().GenericOauth.MapUsername, "username"), MapDisplayName: config.OrDefaultString(app.Config().GenericOauth.MapDisplayName, "-"), MapEmail: config.OrDefaultString(app.Config().GenericOauth.MapEmail, "email"), } configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy) } } func configureGiteaOauth(parentHandler *Handler, r *mux.Router, app *App) { if app.Config().GiteaOauth.ClientID != "" { - callbackLocation := app.Config().App.Host + "/oauth/callback/gitea" + callbackLocation := app.Config().App.AbsoluteURL("/oauth/callback/gitea") var callbackProxy *callbackProxyClient = nil if app.Config().GiteaOauth.CallbackProxy != "" { callbackProxy = &callbackProxyClient{ server: app.Config().GiteaOauth.CallbackProxyAPI, - callbackLocation: app.Config().App.Host + "/oauth/callback/gitea", + callbackLocation: app.Config().App.AbsoluteURL("/oauth/callback/gitea"), httpClient: config.DefaultHTTPClient(), } callbackLocation = app.Config().GiteaOauth.CallbackProxy } oauthClient := giteaOauthClient{ ClientID: app.Config().GiteaOauth.ClientID, ClientSecret: app.Config().GiteaOauth.ClientSecret, ExchangeLocation: app.Config().GiteaOauth.Host + "/login/oauth/access_token", InspectLocation: app.Config().GiteaOauth.Host + "/login/oauth/userinfo", AuthLocation: app.Config().GiteaOauth.Host + "/login/oauth/authorize", HttpClient: config.DefaultHTTPClient(), CallbackLocation: callbackLocation, Scope: "openid profile email", MapUserID: "sub", MapUsername: "login", MapDisplayName: "full_name", MapEmail: "email", } configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy) } } func configureOauthRoutes(parentHandler *Handler, r *mux.Router, app *App, oauthClient oauthClient, callbackProxy *callbackProxyClient) { handler := &oauthHandler{ Config: app.Config(), DB: app.DB(), Store: app.SessionStore(), oauthClient: oauthClient, EmailKey: app.keys.EmailKey, callbackProxy: callbackProxy, } r.HandleFunc("/oauth/"+oauthClient.GetProvider(), parentHandler.OAuth(handler.viewOauthInit)).Methods("GET") r.HandleFunc("/oauth/callback/"+oauthClient.GetProvider(), parentHandler.OAuth(handler.viewOauthCallback)).Methods("GET") r.HandleFunc("/oauth/signup", parentHandler.OAuth(handler.viewOauthSignup)).Methods("POST") } func (h oauthHandler) viewOauthCallback(app *App, w http.ResponseWriter, r *http.Request) error { ctx := r.Context() code := r.FormValue("code") state := r.FormValue("state") provider, clientID, attachUserID, inviteCode, err := h.DB.ValidateOAuthState(ctx, state) if err != nil { log.Error("Unable to ValidateOAuthState: %s", err) return impart.HTTPError{http.StatusInternalServerError, err.Error()} } tokenResponse, err := h.oauthClient.exchangeOauthCode(ctx, code) if err != nil { log.Error("Unable to exchangeOauthCode: %s", err) // TODO: show user friendly message if needed // TODO: show NO message for cases like user pressing "Cancel" on authorize step addSessionFlash(app, w, r, err.Error(), nil) if attachUserID > 0 { return impart.HTTPError{http.StatusFound, "/me/settings"} } return impart.HTTPError{http.StatusInternalServerError, err.Error()} } // Now that we have the access token, let's use it real quick to make sure // it really really works. tokenInfo, err := h.oauthClient.inspectOauthAccessToken(ctx, tokenResponse.AccessToken) if err != nil { log.Error("Unable to inspectOauthAccessToken: %s", err) return impart.HTTPError{http.StatusInternalServerError, err.Error()} } localUserID, err := h.DB.GetIDForRemoteUser(ctx, tokenInfo.UserID, provider, clientID) if err != nil { log.Error("Unable to GetIDForRemoteUser: %s", err) return impart.HTTPError{http.StatusInternalServerError, err.Error()} } if localUserID != -1 && attachUserID > 0 { if err = addSessionFlash(app, w, r, "This OAuth account is already attached to another user.", nil); err != nil { return impart.HTTPError{Status: http.StatusInternalServerError, Message: err.Error()} } return impart.HTTPError{http.StatusFound, "/me/settings"} } if localUserID != -1 { // Existing user, so log in now user, err := h.DB.GetUserByID(localUserID) if err != nil { log.Error("Unable to GetUserByID %d: %s", localUserID, err) return impart.HTTPError{http.StatusInternalServerError, err.Error()} } - if err = loginOrFail(h.Store, w, r, user); err != nil { + if err = loginOrFail(h.Store, w, r, user, app.cfg.App.PrefixPath("/")); err != nil { log.Error("Unable to loginOrFail %d: %s", localUserID, err) return impart.HTTPError{http.StatusInternalServerError, err.Error()} } return nil } if attachUserID > 0 { log.Info("attaching to user %d", attachUserID) log.Info("OAuth userid: %s", tokenInfo.UserID) err = h.DB.RecordRemoteUserID(r.Context(), attachUserID, tokenInfo.UserID, provider, clientID, tokenResponse.AccessToken) if err != nil { return impart.HTTPError{http.StatusInternalServerError, err.Error()} } return impart.HTTPError{http.StatusFound, "/me/settings"} } // New user registration below. // First, verify that user is allowed to register if inviteCode != "" { // Verify invite code is valid i, err := app.db.GetUserInvite(inviteCode) if err != nil { return impart.HTTPError{http.StatusInternalServerError, err.Error()} } if !i.Active(app.db) { return impart.HTTPError{http.StatusNotFound, "Invite link has expired."} } } else if !app.cfg.App.OpenRegistration { addSessionFlash(app, w, r, ErrUserNotFound.Error(), nil) return impart.HTTPError{http.StatusFound, "/login"} } displayName := tokenInfo.DisplayName if len(displayName) == 0 { displayName = tokenInfo.Username } tp := &oauthSignupPageParams{ AccessToken: tokenResponse.AccessToken, TokenUsername: tokenInfo.Username, TokenAlias: tokenInfo.DisplayName, TokenEmail: tokenInfo.Email, TokenRemoteUser: tokenInfo.UserID, Provider: provider, ClientID: clientID, InviteCode: inviteCode, } tp.TokenHash = tp.HashTokenParams(h.Config.Server.HashSeed) return h.showOauthSignupPage(app, w, r, tp, nil) } func (r *callbackProxyClient) register(ctx context.Context, state string) error { form := url.Values{} form.Add("state", state) form.Add("location", r.callbackLocation) req, err := http.NewRequestWithContext(ctx, "POST", r.server, strings.NewReader(form.Encode())) if err != nil { return err } req.Header.Set("User-Agent", ServerUserAgent("")) req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := r.httpClient.Do(req) if err != nil { return err } if resp.StatusCode != http.StatusCreated { return fmt.Errorf("unable register state location: %d", resp.StatusCode) } return nil } func limitedJsonUnmarshal(body io.ReadCloser, n int, thing interface{}) error { lr := io.LimitReader(body, int64(n+1)) data, err := io.ReadAll(lr) if err != nil { return err } if len(data) == n+1 { return fmt.Errorf("content larger than max read allowance: %d", n) } return json.Unmarshal(data, thing) } -func loginOrFail(store sessions.Store, w http.ResponseWriter, r *http.Request, user *User) error { +func loginOrFail(store sessions.Store, w http.ResponseWriter, r *http.Request, user *User, redirectTo string) error { // An error may be returned, but a valid session should always be returned. session, _ := store.Get(r, cookieName) session.Values[cookieUserVal] = user.Cookie() if err := session.Save(r, w); err != nil { fmt.Println("error saving session", err) return err } - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + http.Redirect(w, r, redirectTo, http.StatusTemporaryRedirect) return nil } diff --git a/oauth_generic.go b/oauth_generic.go index 6fa09d7..36f166c 100644 --- a/oauth_generic.go +++ b/oauth_generic.go @@ -1,145 +1,144 @@ /* * Copyright © 2020-2021 Musing Studio LLC and respective authors. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "context" "errors" "fmt" + "github.com/writeas/web-core/log" "net/http" "net/url" "strings" - - "github.com/writeas/web-core/log" ) type genericOauthClient struct { ClientID string ClientSecret string AuthLocation string ExchangeLocation string InspectLocation string CallbackLocation string Scope string MapUserID string MapUsername string MapDisplayName string MapEmail string HttpClient HttpClient } var _ oauthClient = genericOauthClient{} const ( genericOauthDisplayName = "OAuth" ) func (c genericOauthClient) GetProvider() string { return "generic" } func (c genericOauthClient) GetClientID() string { return c.ClientID } func (c genericOauthClient) GetCallbackLocation() string { return c.CallbackLocation } func (c genericOauthClient) buildLoginURL(state string) (string, error) { u, err := url.Parse(c.AuthLocation) if err != nil { return "", err } q := u.Query() q.Set("client_id", c.ClientID) q.Set("redirect_uri", c.CallbackLocation) q.Set("response_type", "code") q.Set("state", state) q.Set("scope", c.Scope) u.RawQuery = q.Encode() return u.String(), nil } func (c genericOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) { form := url.Values{} form.Add("client_id", c.ClientID) form.Add("client_secret", c.ClientSecret) form.Add("grant_type", "authorization_code") form.Add("redirect_uri", c.CallbackLocation) form.Add("scope", c.Scope) form.Add("code", code) req, err := http.NewRequest("POST", c.ExchangeLocation, strings.NewReader(form.Encode())) if err != nil { return nil, err } req.WithContext(ctx) req.Header.Set("User-Agent", ServerUserAgent("")) req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(c.ClientID, c.ClientSecret) resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, errors.New("unable to exchange code for access token") } var tokenResponse TokenResponse if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil { return nil, err } if tokenResponse.Error != "" { return nil, errors.New(tokenResponse.Error) } return &tokenResponse, nil } func (c genericOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) { req, err := http.NewRequest("GET", c.InspectLocation, nil) if err != nil { return nil, err } req.WithContext(ctx) req.Header.Set("User-Agent", ServerUserAgent("")) req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+accessToken) resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, errors.New("unable to inspect access token") } // since we don't know what the JSON from the server will look like, we create a // generic interface and then map manually to values set in the config var genericInterface map[string]interface{} if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &genericInterface); err != nil { return nil, err } // map each relevant field in inspectResponse to the mapped field from the config var inspectResponse InspectResponse inspectResponse.UserID, _ = genericInterface[c.MapUserID].(string) if inspectResponse.UserID == "" { log.Error("[CONFIGURATION ERROR] Generic OAuth provider returned empty UserID value (`%s`).\n Do you need to configure a different `map_user_id` value for this provider?", c.MapUserID) return nil, fmt.Errorf("no UserID (`%s`) value returned", c.MapUserID) } inspectResponse.Username, _ = genericInterface[c.MapUsername].(string) inspectResponse.DisplayName, _ = genericInterface[c.MapDisplayName].(string) inspectResponse.Email, _ = genericInterface[c.MapEmail].(string) return &inspectResponse, nil } diff --git a/oauth_gitea.go b/oauth_gitea.go index 249007f..3ecd4a7 100644 --- a/oauth_gitea.go +++ b/oauth_gitea.go @@ -1,134 +1,133 @@ package writefreely import ( "context" "errors" "fmt" + "github.com/writeas/web-core/log" "net/http" "net/url" "strings" - - "github.com/writeas/web-core/log" ) type giteaOauthClient struct { ClientID string ClientSecret string AuthLocation string ExchangeLocation string InspectLocation string CallbackLocation string Scope string MapUserID string MapUsername string MapDisplayName string MapEmail string HttpClient HttpClient } var _ oauthClient = giteaOauthClient{} const ( giteaDisplayName = "Gitea" ) func (c giteaOauthClient) GetProvider() string { return "gitea" } func (c giteaOauthClient) GetClientID() string { return c.ClientID } func (c giteaOauthClient) GetCallbackLocation() string { return c.CallbackLocation } func (c giteaOauthClient) buildLoginURL(state string) (string, error) { u, err := url.Parse(c.AuthLocation) if err != nil { return "", err } q := u.Query() q.Set("client_id", c.ClientID) q.Set("redirect_uri", c.CallbackLocation) q.Set("response_type", "code") q.Set("state", state) q.Set("scope", c.Scope) u.RawQuery = q.Encode() return u.String(), nil } func (c giteaOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) { form := url.Values{} form.Add("grant_type", "authorization_code") form.Add("redirect_uri", c.CallbackLocation) form.Add("scope", c.Scope) form.Add("code", code) req, err := http.NewRequest("POST", c.ExchangeLocation, strings.NewReader(form.Encode())) if err != nil { return nil, err } req.WithContext(ctx) req.Header.Set("User-Agent", ServerUserAgent("")) req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(c.ClientID, c.ClientSecret) resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, errors.New("unable to exchange code for access token") } var tokenResponse TokenResponse if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil { return nil, err } if tokenResponse.Error != "" { return nil, errors.New(tokenResponse.Error) } return &tokenResponse, nil } func (c giteaOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) { req, err := http.NewRequest("GET", c.InspectLocation, nil) if err != nil { return nil, err } req.WithContext(ctx) req.Header.Set("User-Agent", ServerUserAgent("")) req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+accessToken) resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, errors.New("unable to inspect access token") } // since we don't know what the JSON from the server will look like, we create a // generic interface and then map manually to values set in the config var genericInterface map[string]interface{} if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &genericInterface); err != nil { return nil, err } // map each relevant field in inspectResponse to the mapped field from the config var inspectResponse InspectResponse inspectResponse.UserID, _ = genericInterface[c.MapUserID].(string) // log.Info("Userid from Gitea: %s", inspectResponse.UserID) if inspectResponse.UserID == "" { log.Error("[CONFIGURATION ERROR] Gitea OAuth provider returned empty UserID value (`%s`).\n Do you need to configure a different `map_user_id` value for this provider?", c.MapUserID) return nil, fmt.Errorf("no UserID (`%s`) value returned", c.MapUserID) } inspectResponse.Username, _ = genericInterface[c.MapUsername].(string) inspectResponse.DisplayName, _ = genericInterface[c.MapDisplayName].(string) inspectResponse.Email, _ = genericInterface[c.MapEmail].(string) return &inspectResponse, nil } diff --git a/oauth_signup.go b/oauth_signup.go index b5f881b..356f568 100644 --- a/oauth_signup.go +++ b/oauth_signup.go @@ -1,232 +1,231 @@ /* * Copyright © 2020-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "crypto/sha256" "encoding/hex" "fmt" - "html/template" - "net/http" - "strings" - "time" - "github.com/writeas/impart" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/log" "github.com/writefreely/writefreely/page" + "html/template" + "net/http" + "strings" + "time" ) type viewOauthSignupVars struct { page.StaticPage To string Message template.HTML Flashes []template.HTML AccessToken string TokenUsername string TokenAlias string // TODO: rename this to match the data it represents: the collection title TokenEmail string TokenRemoteUser string Provider string ClientID string TokenHash string InviteCode string LoginUsername string Alias string // TODO: rename this to match the data it represents: the collection title Email string } const ( oauthParamAccessToken = "access_token" oauthParamTokenUsername = "token_username" oauthParamTokenAlias = "token_alias" oauthParamTokenEmail = "token_email" oauthParamTokenRemoteUserID = "token_remote_user" oauthParamClientID = "client_id" oauthParamProvider = "provider" oauthParamHash = "signature" oauthParamUsername = "username" oauthParamAlias = "alias" oauthParamEmail = "email" oauthParamPassword = "password" oauthParamInviteCode = "invite_code" ) type oauthSignupPageParams struct { AccessToken string TokenUsername string TokenAlias string // TODO: rename this to match the data it represents: the collection title TokenEmail string TokenRemoteUser string ClientID string Provider string TokenHash string InviteCode string } func (p oauthSignupPageParams) HashTokenParams(key string) string { hasher := sha256.New() hasher.Write([]byte(key)) hasher.Write([]byte(p.AccessToken)) hasher.Write([]byte(p.TokenUsername)) hasher.Write([]byte(p.TokenAlias)) hasher.Write([]byte(p.TokenEmail)) hasher.Write([]byte(p.TokenRemoteUser)) hasher.Write([]byte(p.ClientID)) hasher.Write([]byte(p.Provider)) return hex.EncodeToString(hasher.Sum(nil)) } func (h oauthHandler) viewOauthSignup(app *App, w http.ResponseWriter, r *http.Request) error { tp := &oauthSignupPageParams{ AccessToken: r.FormValue(oauthParamAccessToken), TokenUsername: r.FormValue(oauthParamTokenUsername), TokenAlias: r.FormValue(oauthParamTokenAlias), TokenEmail: r.FormValue(oauthParamTokenEmail), TokenRemoteUser: r.FormValue(oauthParamTokenRemoteUserID), ClientID: r.FormValue(oauthParamClientID), Provider: r.FormValue(oauthParamProvider), InviteCode: r.FormValue(oauthParamInviteCode), } if tp.HashTokenParams(h.Config.Server.HashSeed) != r.FormValue(oauthParamHash) { return impart.HTTPError{Status: http.StatusBadRequest, Message: "Request has been tampered with."} } tp.TokenHash = tp.HashTokenParams(h.Config.Server.HashSeed) if err := h.validateOauthSignup(r); err != nil { return h.showOauthSignupPage(app, w, r, tp, err) } var err error hashedPass := []byte{} clearPass := r.FormValue(oauthParamPassword) hasPass := clearPass != "" if hasPass { hashedPass, err = auth.HashPass([]byte(clearPass)) if err != nil { return h.showOauthSignupPage(app, w, r, tp, fmt.Errorf("unable to hash password")) } } newUser := &User{ Username: r.FormValue(oauthParamUsername), HashedPass: hashedPass, HasPass: hasPass, Email: prepareUserEmail(r.FormValue(oauthParamEmail), h.EmailKey), Created: time.Now().Truncate(time.Second).UTC(), } displayName := r.FormValue(oauthParamAlias) if len(displayName) == 0 { displayName = r.FormValue(oauthParamUsername) } err = h.DB.CreateUser(h.Config, newUser, displayName, "") if err != nil { return h.showOauthSignupPage(app, w, r, tp, err) } // Log invite if needed if tp.InviteCode != "" { err = app.db.CreateInvitedUser(tp.InviteCode, newUser.ID) if err != nil { return err } } err = h.DB.RecordRemoteUserID(r.Context(), newUser.ID, r.FormValue(oauthParamTokenRemoteUserID), r.FormValue(oauthParamProvider), r.FormValue(oauthParamClientID), r.FormValue(oauthParamAccessToken)) if err != nil { return h.showOauthSignupPage(app, w, r, tp, err) } - if err := loginOrFail(h.Store, w, r, newUser); err != nil { + if err := loginOrFail(h.Store, w, r, newUser, app.cfg.App.PrefixPath("/")); err != nil { return h.showOauthSignupPage(app, w, r, tp, err) } return nil } func (h oauthHandler) validateOauthSignup(r *http.Request) error { username := r.FormValue(oauthParamUsername) if len(username) < h.Config.App.MinUsernameLen { return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too short."} } if len(username) > 100 { return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too long."} } collTitle := r.FormValue(oauthParamAlias) if len(collTitle) == 0 { collTitle = username } email := r.FormValue(oauthParamEmail) if len(email) > 0 { parts := strings.Split(email, "@") if len(parts) != 2 || (len(parts[0]) < 1 || len(parts[1]) < 1) { return impart.HTTPError{Status: http.StatusBadRequest, Message: "Invalid email address"} } } return nil } func (h oauthHandler) showOauthSignupPage(app *App, w http.ResponseWriter, r *http.Request, tp *oauthSignupPageParams, errMsg error) error { username := tp.TokenUsername collTitle := tp.TokenAlias email := tp.TokenEmail session, err := app.sessionStore.Get(r, cookieName) if err != nil { // Ignore this log.Error("Unable to get session; ignoring: %v", err) } if tmpValue := r.FormValue(oauthParamUsername); len(tmpValue) > 0 { username = tmpValue } if tmpValue := r.FormValue(oauthParamAlias); len(tmpValue) > 0 { collTitle = tmpValue } if tmpValue := r.FormValue(oauthParamEmail); len(tmpValue) > 0 { email = tmpValue } p := &viewOauthSignupVars{ StaticPage: pageForReq(app, r), To: r.FormValue("to"), Flashes: []template.HTML{}, AccessToken: tp.AccessToken, TokenUsername: tp.TokenUsername, TokenAlias: tp.TokenAlias, TokenEmail: tp.TokenEmail, TokenRemoteUser: tp.TokenRemoteUser, Provider: tp.Provider, ClientID: tp.ClientID, TokenHash: tp.TokenHash, InviteCode: tp.InviteCode, LoginUsername: username, Alias: collTitle, Email: email, } // Display any error messages flashes, _ := getSessionFlashes(app, w, r, session) for _, flash := range flashes { p.Flashes = append(p.Flashes, template.HTML(flash)) } if errMsg != nil { p.Flashes = append(p.Flashes, template.HTML(errMsg.Error())) } err = pages["signup-oauth.tmpl"].ExecuteTemplate(w, "base", p) if err != nil { log.Error("Unable to render signup-oauth: %v", err) return err } return nil } diff --git a/oauth_slack.go b/oauth_slack.go index d18564a..40f50e4 100644 --- a/oauth_slack.go +++ b/oauth_slack.go @@ -1,179 +1,178 @@ /* * Copyright © 2019-2020 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "context" "errors" + "github.com/gosimple/slug" "net/http" "net/url" "strings" - - "github.com/gosimple/slug" ) type slackOauthClient struct { ClientID string ClientSecret string TeamID string CallbackLocation string HttpClient HttpClient } type slackExchangeResponse struct { OK bool `json:"ok"` AccessToken string `json:"access_token"` Scope string `json:"scope"` TeamName string `json:"team_name"` TeamID string `json:"team_id"` Error string `json:"error"` } type slackIdentity struct { Name string `json:"name"` ID string `json:"id"` Email string `json:"email"` } type slackTeam struct { Name string `json:"name"` ID string `json:"id"` } type slackUserIdentityResponse struct { OK bool `json:"ok"` User slackIdentity `json:"user"` Team slackTeam `json:"team"` Error string `json:"error"` } const ( slackAuthLocation = "https://slack.com/oauth/authorize" slackExchangeLocation = "https://slack.com/api/oauth.access" slackIdentityLocation = "https://slack.com/api/users.identity" ) var _ oauthClient = slackOauthClient{} func (c slackOauthClient) GetProvider() string { return "slack" } func (c slackOauthClient) GetClientID() string { return c.ClientID } func (c slackOauthClient) GetCallbackLocation() string { return c.CallbackLocation } func (c slackOauthClient) buildLoginURL(state string) (string, error) { u, err := url.Parse(slackAuthLocation) if err != nil { return "", err } q := u.Query() q.Set("client_id", c.ClientID) q.Set("scope", "identity.basic identity.email identity.team") q.Set("redirect_uri", c.CallbackLocation) q.Set("state", state) // If this param is not set, the user can select which team they // authenticate through and then we'd have to match the configured team // against the profile get. That is extra work in the post-auth phase // that we don't want to do. q.Set("team", c.TeamID) // The Slack OAuth docs don't explicitly list this one, but it is part of // the spec, so we include it anyway. q.Set("response_type", "code") u.RawQuery = q.Encode() return u.String(), nil } func (c slackOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) { form := url.Values{} // The oauth.access documentation doesn't explicitly mention this // parameter, but it is part of the spec, so we include it anyway. // https://api.slack.com/methods/oauth.access form.Add("grant_type", "authorization_code") form.Add("redirect_uri", c.CallbackLocation) form.Add("code", code) req, err := http.NewRequest("POST", slackExchangeLocation, strings.NewReader(form.Encode())) if err != nil { return nil, err } req.WithContext(ctx) req.Header.Set("User-Agent", ServerUserAgent("")) req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(c.ClientID, c.ClientSecret) resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, errors.New("unable to exchange code for access token") } var tokenResponse slackExchangeResponse if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil { return nil, err } if !tokenResponse.OK { return nil, errors.New(tokenResponse.Error) } return tokenResponse.TokenResponse(), nil } func (c slackOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) { req, err := http.NewRequest("GET", slackIdentityLocation, nil) if err != nil { return nil, err } req.WithContext(ctx) req.Header.Set("User-Agent", ServerUserAgent("")) req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+accessToken) resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, errors.New("unable to inspect access token") } var inspectResponse slackUserIdentityResponse if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil { return nil, err } if !inspectResponse.OK { return nil, errors.New(inspectResponse.Error) } return inspectResponse.InspectResponse(), nil } func (resp slackUserIdentityResponse) InspectResponse() *InspectResponse { return &InspectResponse{ UserID: resp.User.ID, Username: slug.Make(resp.User.Name), DisplayName: resp.User.Name, Email: resp.User.Email, } } func (resp slackExchangeResponse) TokenResponse() *TokenResponse { return &TokenResponse{ AccessToken: resp.AccessToken, } } diff --git a/oauth_test.go b/oauth_test.go index 326e7ed..2cad841 100644 --- a/oauth_test.go +++ b/oauth_test.go @@ -1,262 +1,270 @@ /* * Copyright © 2019-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "context" "fmt" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - "github.com/gorilla/sessions" "github.com/stretchr/testify/assert" "github.com/writeas/impart" "github.com/writeas/web-core/id" "github.com/writefreely/writefreely/config" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" ) type MockOAuthDatastoreProvider struct { DoDB func() OAuthDatastore DoConfig func() *config.Config DoSessionStore func() sessions.Store } type MockOAuthDatastore struct { DoGenerateOAuthState func(context.Context, string, string, int64, string) (string, error) DoValidateOAuthState func(context.Context, string) (string, string, int64, string, error) DoGetIDForRemoteUser func(context.Context, string, string, string) (int64, error) DoCreateUser func(*config.Config, *User, string) error DoRecordRemoteUserID func(context.Context, int64, string, string, string, string) error DoGetUserByID func(int64) (*User, error) } var _ OAuthDatastore = &MockOAuthDatastore{} type StringReadCloser struct { *strings.Reader } func (src *StringReadCloser) Close() error { return nil } type MockHTTPClient struct { DoDo func(req *http.Request) (*http.Response, error) } func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) { if m.DoDo != nil { return m.DoDo(req) } return &http.Response{}, nil } func (m *MockOAuthDatastoreProvider) SessionStore() sessions.Store { if m.DoSessionStore != nil { return m.DoSessionStore() } return sessions.NewCookieStore([]byte("secret-key")) } func (m *MockOAuthDatastoreProvider) DB() OAuthDatastore { if m.DoDB != nil { return m.DoDB() } return &MockOAuthDatastore{} } func (m *MockOAuthDatastoreProvider) Config() *config.Config { if m.DoConfig != nil { return m.DoConfig() } cfg := config.New() cfg.UseSQLite(true) cfg.WriteAsOauth = config.WriteAsOauthCfg{ ClientID: "development", ClientSecret: "development", AuthLocation: "https://write.as/oauth/login", TokenLocation: "https://write.as/oauth/token", InspectLocation: "https://write.as/oauth/inspect", } cfg.SlackOauth = config.SlackOauthCfg{ ClientID: "development", ClientSecret: "development", TeamID: "development", } return cfg } func (m *MockOAuthDatastore) ValidateOAuthState(ctx context.Context, state string) (string, string, int64, string, error) { if m.DoValidateOAuthState != nil { return m.DoValidateOAuthState(ctx, state) } return "", "", 0, "", nil } func (m *MockOAuthDatastore) GetIDForRemoteUser(ctx context.Context, remoteUserID, provider, clientID string) (int64, error) { if m.DoGetIDForRemoteUser != nil { return m.DoGetIDForRemoteUser(ctx, remoteUserID, provider, clientID) } return -1, nil } func (m *MockOAuthDatastore) CreateUser(cfg *config.Config, u *User, username, description string) error { if m.DoCreateUser != nil { return m.DoCreateUser(cfg, u, username) } u.ID = 1 return nil } func (m *MockOAuthDatastore) RecordRemoteUserID(ctx context.Context, localUserID int64, remoteUserID, provider, clientID, accessToken string) error { if m.DoRecordRemoteUserID != nil { return m.DoRecordRemoteUserID(ctx, localUserID, remoteUserID, provider, clientID, accessToken) } return nil } func (m *MockOAuthDatastore) GetUserByID(userID int64) (*User, error) { if m.DoGetUserByID != nil { return m.DoGetUserByID(userID) } user := &User{} return user, nil } func (m *MockOAuthDatastore) GenerateOAuthState(ctx context.Context, provider string, clientID string, attachUserID int64, inviteCode string) (string, error) { if m.DoGenerateOAuthState != nil { return m.DoGenerateOAuthState(ctx, provider, clientID, attachUserID, inviteCode) } return id.Generate62RandomString(14), nil } func TestViewOauthInit(t *testing.T) { t.Run("success", func(t *testing.T) { app := &MockOAuthDatastoreProvider{} h := oauthHandler{ Config: app.Config(), DB: app.DB(), Store: app.SessionStore(), EmailKey: []byte{0xd, 0xe, 0xc, 0xa, 0xf, 0xf, 0xb, 0xa, 0xd}, oauthClient: writeAsOauthClient{ ClientID: app.Config().WriteAsOauth.ClientID, ClientSecret: app.Config().WriteAsOauth.ClientSecret, ExchangeLocation: app.Config().WriteAsOauth.TokenLocation, InspectLocation: app.Config().WriteAsOauth.InspectLocation, AuthLocation: app.Config().WriteAsOauth.AuthLocation, CallbackLocation: "http://localhost/oauth/callback", HttpClient: nil, }, } req, err := http.NewRequest("GET", "/oauth/client", nil) assert.NoError(t, err) rr := httptest.NewRecorder() err = h.viewOauthInit(nil, rr, req) assert.NotNil(t, err) httpErr, ok := err.(impart.HTTPError) assert.True(t, ok) assert.Equal(t, http.StatusTemporaryRedirect, httpErr.Status) assert.NotEmpty(t, httpErr.Message) locURI, err := url.Parse(httpErr.Message) assert.NoError(t, err) assert.Equal(t, "/oauth/login", locURI.Path) assert.Equal(t, "development", locURI.Query().Get("client_id")) assert.Equal(t, "http://localhost/oauth/callback", locURI.Query().Get("redirect_uri")) assert.Equal(t, "code", locURI.Query().Get("response_type")) assert.NotEmpty(t, locURI.Query().Get("state")) }) t.Run("state failure", func(t *testing.T) { app := &MockOAuthDatastoreProvider{ DoDB: func() OAuthDatastore { return &MockOAuthDatastore{ DoGenerateOAuthState: func(ctx context.Context, provider, clientID string, attachUserID int64, inviteCode string) (string, error) { return "", fmt.Errorf("pretend unable to write state error") }, } }, } h := oauthHandler{ Config: app.Config(), DB: app.DB(), Store: app.SessionStore(), EmailKey: []byte{0xd, 0xe, 0xc, 0xa, 0xf, 0xf, 0xb, 0xa, 0xd}, oauthClient: writeAsOauthClient{ ClientID: app.Config().WriteAsOauth.ClientID, ClientSecret: app.Config().WriteAsOauth.ClientSecret, ExchangeLocation: app.Config().WriteAsOauth.TokenLocation, InspectLocation: app.Config().WriteAsOauth.InspectLocation, AuthLocation: app.Config().WriteAsOauth.AuthLocation, CallbackLocation: "http://localhost/oauth/callback", HttpClient: nil, }, } req, err := http.NewRequest("GET", "/oauth/client", nil) assert.NoError(t, err) rr := httptest.NewRecorder() err = h.viewOauthInit(nil, rr, req) httpErr, ok := err.(impart.HTTPError) assert.True(t, ok) assert.NotEmpty(t, httpErr.Message) assert.Equal(t, http.StatusInternalServerError, httpErr.Status) assert.Equal(t, "could not prepare oauth redirect url", httpErr.Message) }) } func TestViewOauthCallback(t *testing.T) { t.Run("success", func(t *testing.T) { - app := &MockOAuthDatastoreProvider{} + app := &MockOAuthDatastoreProvider{ + DoDB: func() OAuthDatastore { + return &MockOAuthDatastore{ + DoGetIDForRemoteUser: func(ctx context.Context, remoteUserID, provider, clientID string) (int64, error) { + return 1, nil + }, + } + }, + } h := oauthHandler{ Config: app.Config(), DB: app.DB(), Store: app.SessionStore(), EmailKey: []byte{0xd, 0xe, 0xc, 0xa, 0xf, 0xf, 0xb, 0xa, 0xd}, oauthClient: writeAsOauthClient{ ClientID: app.Config().WriteAsOauth.ClientID, ClientSecret: app.Config().WriteAsOauth.ClientSecret, ExchangeLocation: app.Config().WriteAsOauth.TokenLocation, InspectLocation: app.Config().WriteAsOauth.InspectLocation, AuthLocation: app.Config().WriteAsOauth.AuthLocation, CallbackLocation: "http://localhost/oauth/callback", HttpClient: &MockHTTPClient{ DoDo: func(req *http.Request) (*http.Response, error) { switch req.URL.String() { case "https://write.as/oauth/token": return &http.Response{ StatusCode: 200, Body: &StringReadCloser{strings.NewReader(`{"access_token": "access_token", "expires_in": 1000, "refresh_token": "refresh_token", "token_type": "access"}`)}, }, nil case "https://write.as/oauth/inspect": return &http.Response{ StatusCode: 200, Body: &StringReadCloser{strings.NewReader(`{"client_id": "development", "user_id": "1", "expires_at": "2019-12-19T11:42:01Z", "username": "nick", "email": "nick@testing.write.as"}`)}, }, nil } return &http.Response{ StatusCode: http.StatusNotFound, }, nil }, }, }, } req, err := http.NewRequest("GET", "/oauth/callback", nil) assert.NoError(t, err) rr := httptest.NewRecorder() err = h.viewOauthCallback(&App{cfg: app.Config(), sessionStore: app.SessionStore()}, rr, req) assert.NoError(t, err) assert.Equal(t, http.StatusTemporaryRedirect, rr.Code) + assert.Equal(t, "/", rr.Header().Get("Location")) }) } diff --git a/page/page.go b/page/page.go index 057f034..5ab7750 100644 --- a/page/page.go +++ b/page/page.go @@ -1,49 +1,48 @@ /* * Copyright © 2018-2019, 2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ // package page provides mechanisms and data for generating a WriteFreely page. package page import ( - "strings" - "github.com/writefreely/writefreely/config" + "strings" ) type StaticPage struct { // App configuration config.AppCfg Version string HeaderNav bool CustomCSS bool // Request values Path string Username string Values map[string]string Flashes []string CanViewReader bool IsAdmin bool CanInvite bool } // SanitizeHost alters the StaticPage to contain a real hostname. This is // especially important for the Tor hidden service, as it can be served over // proxies, messing up the apparent hostname. func (sp *StaticPage) SanitizeHost(cfg *config.Config) { if cfg.Server.HiddenHost != "" && strings.HasPrefix(sp.Host, cfg.Server.HiddenHost) { sp.Host = cfg.Server.HiddenHost } } func (sp StaticPage) OfficialVersion() string { p := strings.Split(sp.Version, "-") return p[0] } diff --git a/pages.go b/pages.go index d835545..6538e7a 100644 --- a/pages.go +++ b/pages.go @@ -1,199 +1,198 @@ /* * Copyright © 2018-2019, 2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "database/sql" - "time" - "github.com/writefreely/writefreely/config" + "time" ) var defaultPageUpdatedTime = time.Date(2018, 11, 8, 12, 0, 0, 0, time.Local) func getAboutPage(app *App) (*instanceContent, error) { c, err := app.db.GetDynamicContent("about") if err != nil { return nil, err } if c == nil { c = &instanceContent{ ID: "about", Type: "page", Content: defaultAboutPage(app.cfg), } } if !c.Title.Valid { c.Title = defaultAboutTitle(app.cfg) } return c, nil } func defaultAboutTitle(cfg *config.Config) sql.NullString { return sql.NullString{String: "About " + cfg.App.SiteName, Valid: true} } func getContactPage(app *App) (*instanceContent, error) { c, err := app.db.GetDynamicContent("contact") if err != nil { return nil, err } if c == nil { c = &instanceContent{ ID: "contact", Type: "page", Content: defaultContactPage(app), } } if !c.Title.Valid { c.Title = defaultContactTitle() } return c, nil } func defaultContactTitle() sql.NullString { return sql.NullString{String: "Contact Us", Valid: true} } func getPrivacyPage(app *App) (*instanceContent, error) { c, err := app.db.GetDynamicContent("privacy") if err != nil { return nil, err } if c == nil { c = &instanceContent{ ID: "privacy", Type: "page", Content: defaultPrivacyPolicy(app.cfg), Updated: defaultPageUpdatedTime, } } if !c.Title.Valid { c.Title = defaultPrivacyTitle() } return c, nil } func defaultPrivacyTitle() sql.NullString { return sql.NullString{String: "Privacy Policy", Valid: true} } func defaultAboutPage(cfg *config.Config) string { if cfg.App.Federation { return `_` + cfg.App.SiteName + `_ is an interconnected place for you to write and publish, powered by [WriteFreely](https://writefreely.org) and ActivityPub.` } return `_` + cfg.App.SiteName + `_ is a place for you to write and publish, powered by [WriteFreely](https://writefreely.org).` } func defaultContactPage(app *App) string { c, err := app.db.GetCollectionByID(1) if err != nil { return "" } - return `_` + app.cfg.App.SiteName + `_ is administered by: [**` + c.Alias + `**](/` + c.Alias + `/). + return `_` + app.cfg.App.SiteName + `_ is administered by: [**` + c.Alias + `**](` + app.cfg.App.PrefixPath("/"+c.Alias+"/") + `). Contact them at this email address: _EMAIL GOES HERE_. You can also reach them here...` } func defaultPrivacyPolicy(cfg *config.Config) string { return `[WriteFreely](https://writefreely.org), the software that powers this site, is built to enforce your right to privacy by default. It retains as little data about you as possible, not even requiring an email address to sign up. However, if you _do_ give us your email address, it is stored encrypted in our database. We salt and hash your account's password. We store log files, or data about what happens on our servers. We also use cookies to keep you logged into your account. Beyond this, it's important that you trust whoever runs **` + cfg.App.SiteName + `**. Software can only do so much to protect you -- your level of privacy protections will ultimately fall on the humans that run this particular service.` } func getLandingBanner(app *App) (*instanceContent, error) { c, err := app.db.GetDynamicContent("landing-banner") if err != nil { return nil, err } if c == nil { c = &instanceContent{ ID: "landing-banner", Type: "section", Content: defaultLandingBanner(app.cfg), Updated: defaultPageUpdatedTime, } } return c, nil } func getLandingBody(app *App) (*instanceContent, error) { c, err := app.db.GetDynamicContent("landing-body") if err != nil { return nil, err } if c == nil { c = &instanceContent{ ID: "landing-body", Type: "section", Content: defaultLandingBody(app.cfg), Updated: defaultPageUpdatedTime, } } return c, nil } func defaultLandingBanner(cfg *config.Config) string { if cfg.App.Federation { return "# Start your blog in the fediverse" } return "# Start your blog" } func defaultLandingBody(cfg *config.Config) string { if cfg.App.Federation { return `## Join the Fediverse The fediverse is a large network of platforms that all speak a common language. Imagine if you could reply to Instagram posts from Twitter, or interact with your favorite Medium blogs from Facebook -- federated alternatives like [PixelFed](https://pixelfed.org), [Mastodon](https://joinmastodon.org), and WriteFreely enable you to do these types of things.
## Write More Socially WriteFreely can communicate with other federated platforms like Mastodon, so people can follow your blogs, bookmark their favorite posts, and boost them to their followers. Sign up above to create a blog and join the fediverse.` } return "" } func getReaderSection(app *App) (*instanceContent, error) { c, err := app.db.GetDynamicContent("reader") if err != nil { return nil, err } if c == nil { c = &instanceContent{ ID: "reader", Type: "section", Content: defaultReaderBanner(app.cfg), Updated: defaultPageUpdatedTime, } } if !c.Title.Valid { c.Title = defaultReaderTitle(app.cfg) } return c, nil } func defaultReaderTitle(cfg *config.Config) sql.NullString { return sql.NullString{String: "Reader", Valid: true} } func defaultReaderBanner(cfg *config.Config) string { return "Read the latest posts from " + cfg.App.SiteName + "." } diff --git a/pages/404.tmpl b/pages/404.tmpl index b103e27..cf9051c 100644 --- a/pages/404.tmpl +++ b/pages/404.tmpl @@ -1,10 +1,10 @@ {{define "head"}}Post not found — {{.SiteName}}{{end}} {{define "content"}}

Post not found.

{{if and (not .SingleUser) .OpenRegistration}}

Why not share a thought of your own?

-

Start a blog and spread your ideas on {{.SiteName}}, a simple{{if .Federation}}, federated{{end}} blogging community.

+

Start a blog and spread your ideas on {{.SiteName}}, a simple{{if .Federation}}, federated{{end}} blogging community.

{{end}}
{{end}} diff --git a/pages/500.tmpl b/pages/500.tmpl index 4240348..c98c066 100644 --- a/pages/500.tmpl +++ b/pages/500.tmpl @@ -1,8 +1,8 @@ {{define "head"}}Server error — {{.SiteName}}{{end}} {{define "content"}}

Server error 😵

-

There seems to be an issue with this server. Please contact the admin and let them know they'll need to fix it.

+

There seems to be an issue with this server. Please contact the admin and let them know they'll need to fix it.

– {{.SiteName}} 🤖

{{end}} diff --git a/pages/landing.tmpl b/pages/landing.tmpl index 2131b40..4ddaed9 100644 --- a/pages/landing.tmpl +++ b/pages/landing.tmpl @@ -1,203 +1,203 @@ {{define "head"}} {{.SiteName}} {{end}} {{define "content"}}
{{ if .OpenRegistration }} {{template "oauth-buttons" .}} {{if not .DisablePasswordAuth}} {{if .Flashes}}
    {{range .Flashes}}
  • {{.}}
  • {{end}}
{{end}}
-
+
{{end}} {{ else }}

Registration is currently closed.

You can always sign up on another instance.

{{ end }}
{{if .Content}}
{{end}} {{ if .Content }}
{{.Content}}
{{ end }} - + {{end}} diff --git a/pages/login.tmpl b/pages/login.tmpl index c908613..b68993b 100644 --- a/pages/login.tmpl +++ b/pages/login.tmpl @@ -1,43 +1,43 @@ {{define "head"}}Log in — {{.SiteName}} {{end}} {{define "content"}}

Log into {{.SiteName}}

{{if .Flashes}}
    {{range .Flashes}}
  • {{.}}
  • {{end}}
{{end}} {{template "oauth-buttons" .}} {{if not .DisablePasswordAuth}} -
+

- {{if .EmailEnabled}}

Forgot password?

{{end}} + {{if .EmailEnabled}}

Forgot password?

{{end}} {{if .To}}{{end}}
{{if and (not .SingleUser) .OpenRegistration}}

{{if .Message}}{{.Message}}{{else}}No account yet? Sign up to start a blog.{{end}}

{{end}} {{end}} {{end}} diff --git a/pages/reset.tmpl b/pages/reset.tmpl index bc18377..0b40e72 100644 --- a/pages/reset.tmpl +++ b/pages/reset.tmpl @@ -1,58 +1,58 @@ {{define "head"}}Reset password — {{.SiteName}} {{end}} {{define "content"}}

Reset your password

{{ if .DisablePasswordAuth }}

Password login is disabled on this server, so it's not possible to reset your password.

{{ else if not .EmailEnabled }}
-

Email is not configured on this server! Please contact your admin to reset your password.

+

Email is not configured on this server! Please contact your admin to reset your password.

{{ else }} {{if .Flashes}}
    {{range .Flashes}}
  • {{.}}
  • {{end}}
{{end}} {{if .IsResetting}} -
+ {{ .CSRFField }}
{{else if not .IsSent}} -
+ {{ .CSRFField }}
{{end}} {{ end }} {{end}} diff --git a/pages/signup-oauth.tmpl b/pages/signup-oauth.tmpl index fcd70d2..6c5d95c 100644 --- a/pages/signup-oauth.tmpl +++ b/pages/signup-oauth.tmpl @@ -1,186 +1,186 @@ {{define "head"}}Finish Creating Account — {{.SiteName}} {{end}} {{define "content"}}

Finish creating account

{{if .Flashes}}
    {{range .Flashes}}
  • {{.}}
  • {{end}}
{{end}}
-
+ {{if .InviteCode}}{{end}}
- + {{end}} diff --git a/pages/signup.tmpl b/pages/signup.tmpl index b1bb50d..b0b9ffe 100644 --- a/pages/signup.tmpl +++ b/pages/signup.tmpl @@ -1,180 +1,180 @@ {{define "head"}} Sign up — {{.SiteName}} {{end}} {{define "content"}}

Sign up

{{ if .Error }}

{{.Error}}

{{ else }} {{if .Flashes}}
    {{range .Flashes}}
  • {{.}}
  • {{end}}
{{end}}
{{template "oauth-buttons" .}} {{if not .DisablePasswordAuth}} -
+
{{end}}
{{ end }}
- + {{end}} diff --git a/parse/posts.go b/parse/posts.go index d28c5fd..5a39a8d 100644 --- a/parse/posts.go +++ b/parse/posts.go @@ -1,92 +1,91 @@ /* * Copyright © 2018-2020 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ // Package parse assists in the parsing of plain text posts package parse import ( + "github.com/writeas/web-core/stringmanip" "regexp" "strings" - - "github.com/writeas/web-core/stringmanip" ) var ( titleElementReg = regexp.MustCompile("") urlReg = regexp.MustCompile("https?://") imgReg = regexp.MustCompile(`!\[([^]]+)\]\([^)]+\)`) ) // PostLede attempts to extract the first thought of the given post, generally // contained within the first line or sentence of text. func PostLede(t string, includePunc bool) string { // Adjust where we truncate if we want to include punctuation iAdj := 0 if includePunc { iAdj = 1 } // Find lede within first line of text nl := strings.IndexRune(t, '\n') if nl > -1 { t = t[:nl] } // Strip certain HTML tags t = titleElementReg.ReplaceAllString(t, "") // Strip URL protocols t = urlReg.ReplaceAllString(t, "") // Strip image URL, leaving only alt text t = imgReg.ReplaceAllString(t, " $1 ") // Find lede within first sentence punc := strings.Index(t, ". ") if punc > -1 { t = t[:punc+iAdj] } punc = stringmanip.IndexRune(t, '。') if punc > -1 { c := []rune(t) t = string(c[:punc+iAdj]) } punc = stringmanip.IndexRune(t, '?') if punc > -1 { c := []rune(t) t = string(c[:punc+iAdj]) } return t } // TruncToWord truncates the given text to the provided limit. func TruncToWord(s string, l int) (string, bool) { truncated := false c := []rune(s) if len(c) > l { truncated = true s = string(c[:l]) spaceIdx := strings.LastIndexByte(s, ' ') if spaceIdx > -1 { s = s[:spaceIdx] } } return s, truncated } // Truncate truncates the given text to the provided limit, returning the original string if it's shorter than the limit. func Truncate(s string, l int) string { c := []rune(s) if len(c) > l { s = string(c[:l]) } return s } diff --git a/postrender.go b/postrender.go index f4f6dd7..7c71e8c 100644 --- a/postrender.go +++ b/postrender.go @@ -1,381 +1,365 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "bytes" "encoding/json" "fmt" "html" "html/template" "net/http" "net/url" "regexp" "strings" "unicode" "unicode/utf8" "github.com/microcosm-cc/bluemonday" stripmd "github.com/writeas/go-strip-markdown/v2" "github.com/writeas/impart" blackfriday "github.com/writeas/saturday" "github.com/writeas/web-core/log" "github.com/writeas/web-core/stringmanip" "github.com/writefreely/writefreely/config" "github.com/writefreely/writefreely/parse" - "github.com/writefreely/writefreely/spam" ) var ( blockReg = regexp.MustCompile("<(ul|ol|blockquote)>\n") endBlockReg = regexp.MustCompile("\n") youtubeReg = regexp.MustCompile("(https?://www.youtube.com/embed/[a-zA-Z0-9\\-_]+)(\\?[^\t\n\f\r \"']+)?") titleElementReg = regexp.MustCompile("") hashtagReg = regexp.MustCompile(`{{\[\[\|\|([^|]+)\|\|\]\]}}`) markeddownReg = regexp.MustCompile("

(.+)

") mentionReg = regexp.MustCompile(`@([A-Za-z0-9._%+-]+)(@[A-Za-z0-9.-]+\.[A-Za-z]+)\b`) ) func (p *Post) handlePremiumContent(c *Collection, isOwner, postPage bool, cfg *config.Config) { if c.Monetization != "" { // User has Web Monetization enabled, so split content if it exists spl := strings.Index(p.Content, shortCodePaid) p.IsPaid = spl > -1 if postPage { // We're viewing the individual post if isOwner { p.Content = strings.Replace(p.Content, shortCodePaid, "\n\n"+`

Your subscriber content begins here.

`+"\n\n", 1) } else { if spl > -1 { p.Content = p.Content[:spl+len(shortCodePaid)] p.Content = strings.Replace(p.Content, shortCodePaid, "\n\n"+`

Continue reading with a Coil membership.

`+"\n\n", 1) } } } else { // We've viewing the post on the collection landing if spl > -1 { baseURL := c.CanonicalURL() if isOwner { - baseURL = "/" + c.Alias + "/" + baseURL = c.Alias + "/" } p.Content = p.Content[:spl+len(shortCodePaid)] p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:spl]), baseURL, cfg)) } } } } func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool, isPostPage bool) { baseURL := c.CanonicalURL() // TODO: redundant if !isSingleUser { - baseURL = "/" + c.Alias + "/" + baseURL = c.Alias + "/" } p.handlePremiumContent(c, isOwner, isPostPage, cfg) p.Content = strings.Replace(p.Content, "<!--paid-->", "", 1) p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String))) p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), baseURL, cfg)) if exc := strings.Index(string(p.Content), ""); exc > -1 { p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:exc]), baseURL, cfg)) } } func (p *PublicPost) formatContent(cfg *config.Config, isOwner bool, isPostPage bool) { p.Post.formatContent(cfg, &p.Collection.Collection, isOwner, isPostPage) } func (p *Post) augmentContent(c *Collection) { if p.PinnedPosition.Valid { // Don't augment posts that are pinned return } if strings.Index(p.Content, shortCodeNoSig) > -1 { // Don't augment posts with the special "nosig" shortcode return } // Add post signatures if c.Signature != "" { p.Content += "\n\n" + c.Signature } } func (p *PublicPost) augmentContent() { p.Post.augmentContent(&p.Collection.Collection) } func (p *PublicPost) augmentReadingDestination() { if p.IsPaid { p.HTMLContent += template.HTML("\n\n" + `

` + localStr("Read more...", p.Language.String) + ` ($)

`) } } func applyMarkdown(data []byte, baseURL string, cfg *config.Config) string { return applyMarkdownSpecial(data, baseURL, cfg, cfg.App.SingleUser) } func disableYoutubeAutoplay(outHTML string) string { for _, match := range youtubeReg.FindAllString(outHTML, -1) { u, err := url.Parse(match) if err != nil { continue } u.RawQuery = html.UnescapeString(u.RawQuery) q := u.Query() // Set Youtube autoplay url parameter, if any, to 0 if len(q["autoplay"]) == 1 { q.Set("autoplay", "0") } u.RawQuery = q.Encode() cleanURL := u.String() outHTML = strings.Replace(outHTML, match, cleanURL, 1) } return outHTML } func applyMarkdownSpecial(data []byte, baseURL string, cfg *config.Config, skipNoFollow bool) string { mdExtensions := 0 | blackfriday.EXTENSION_TABLES | blackfriday.EXTENSION_FENCED_CODE | blackfriday.EXTENSION_AUTOLINK | blackfriday.EXTENSION_STRIKETHROUGH | blackfriday.EXTENSION_SPACE_HEADERS | blackfriday.EXTENSION_AUTO_HEADER_IDS htmlFlags := 0 | blackfriday.HTML_USE_SMARTYPANTS | blackfriday.HTML_SMARTYPANTS_DASHES if baseURL != "" { htmlFlags |= blackfriday.HTML_HASHTAGS } // Generate Markdown md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) if baseURL != "" { // Replace special text generated by Markdown parser tagPrefix := baseURL + "tag:" if cfg.App.Chorus { - tagPrefix = "/read/t/" + tagPrefix = cfg.App.PrefixPath("/read/t/") } md = []byte(hashtagReg.ReplaceAll(md, []byte("#$1"))) - handlePrefix := cfg.App.Host + "/@/" + handlePrefix := cfg.App.AbsoluteURL("/@/") md = []byte(mentionReg.ReplaceAll(md, []byte("@$1$2"))) } // Strip out bad HTML policy := getSanitizationPolicy() policy.RequireNoFollowOnLinks(!skipNoFollow) outHTML := string(policy.SanitizeBytes(md)) // Strip newlines on certain block elements that render with them outHTML = blockReg.ReplaceAllString(outHTML, "<$1>") outHTML = endBlockReg.ReplaceAllString(outHTML, "") outHTML = disableYoutubeAutoplay(outHTML) return outHTML } func applyBasicMarkdown(data []byte) string { if len(bytes.TrimSpace(data)) == 0 { return "" } mdExtensions := 0 | blackfriday.EXTENSION_STRIKETHROUGH | blackfriday.EXTENSION_SPACE_HEADERS | blackfriday.EXTENSION_HEADER_IDS htmlFlags := 0 | blackfriday.HTML_SKIP_HTML | blackfriday.HTML_USE_SMARTYPANTS | blackfriday.HTML_SMARTYPANTS_DASHES // Generate Markdown // This passes the supplied title into blackfriday.Markdown() as an H1 header, so we only render HTML that // belongs in an H1. md := blackfriday.Markdown(append([]byte("# "), data...), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) // Remove H1 markup md = bytes.TrimSpace(md) // blackfriday.Markdown adds a newline at the end of the

if len(md) == 0 { return "" } md = md[len("

") : len(md)-len("

")] // Strip out bad HTML policy := bluemonday.UGCPolicy() policy.AllowAttrs("class", "id").Globally() outHTML := string(policy.SanitizeBytes(md)) outHTML = markeddownReg.ReplaceAllString(outHTML, "$1") outHTML = strings.TrimRightFunc(outHTML, unicode.IsSpace) return outHTML } func postTitle(content, friendlyId string) string { const maxTitleLen = 80 content = stripHTMLWithoutEscaping(content) content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace) eol := strings.IndexRune(content, '\n') blankLine := strings.Index(content, "\n\n") if blankLine != -1 && blankLine <= eol && blankLine <= assumedTitleLen { return strings.TrimSpace(content[:blankLine]) } else if utf8.RuneCountInString(content) <= maxTitleLen { return content } return friendlyId } // TODO: fix duplicated code from postTitle. postTitle is a widely used func we // don't have time to investigate right now. func friendlyPostTitle(content, friendlyId string) string { const maxTitleLen = 80 content = stripHTMLWithoutEscaping(content) content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace) eol := strings.IndexRune(content, '\n') blankLine := strings.Index(content, "\n\n") if blankLine != -1 && blankLine <= eol && blankLine <= assumedTitleLen { return strings.TrimSpace(content[:blankLine]) } else if eol == -1 && utf8.RuneCountInString(content) <= maxTitleLen { return content } title, truncd := parse.TruncToWord(parse.PostLede(content, true), maxTitleLen) if truncd { title += "..." } return title } // Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML // entities added in by sanitizing the content. func stripHTMLWithoutEscaping(content string) string { return html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content)) } func getSanitizationPolicy() *bluemonday.Policy { policy := bluemonday.UGCPolicy() policy.AllowAttrs("src", "style").OnElements("iframe", "video", "audio") policy.AllowAttrs("src", "type").OnElements("source") policy.AllowAttrs("frameborder", "width", "height").Matching(bluemonday.Integer).OnElements("iframe") policy.AllowAttrs("allowfullscreen").OnElements("iframe") policy.AllowAttrs("controls", "loop", "muted", "autoplay").OnElements("video") policy.AllowAttrs("controls", "loop", "muted", "autoplay", "preload").OnElements("audio") policy.AllowAttrs("target").OnElements("a") policy.AllowAttrs("title").OnElements("abbr") policy.AllowAttrs("style", "class", "id").Globally() policy.AllowAttrs("alt").OnElements("img") policy.AllowElements("header", "footer") policy.AllowAttrs("method", "action").OnElements("form") policy.AllowAttrs("type", "name", "value", "placeholder").OnElements("input") - policy.AllowURLSchemes("http", "https", "mailto", "xmpp", "gopher", "gophers", "gemini", "spartan") + policy.AllowURLSchemes("http", "https", "mailto", "xmpp") return policy } func sanitizePost(content string) string { return strings.Replace(content, "<", "<", -1) } // postDescription generates a description based on the given post content, // title, and post ID. This doesn't consider a V2 post field, `title` when // choosing what to generate. In case a post has a title, this function will // fail, and logic should instead be implemented to skip this when there's no // title, like so: // // var desc string // if title == "" { // desc = postDescription(content, title, friendlyId) // } else { // desc = shortPostDescription(content) // } func postDescription(content, title, friendlyId string) string { maxLen := 140 if content == "" { content = "WriteFreely is a painless, simple, federated blogging platform." } else { fmtStr := "%s" truncation := 0 if utf8.RuneCountInString(content) > maxLen { // Post is longer than the max description, so let's show a better description fmtStr = "%s..." truncation = 3 } if title == friendlyId { // No specific title was found; simply truncate the post, starting at the beginning content = fmt.Sprintf(fmtStr, strings.Replace(stringmanip.Substring(content, 0, maxLen-truncation), "\n", " ", -1)) } else { // There was a title, so return a real description blankLine := strings.Index(content, "\n\n") if blankLine < 0 { blankLine = 0 } truncd := stringmanip.Substring(content, blankLine, blankLine+maxLen-truncation) contentNoNL := strings.Replace(truncd, "\n", " ", -1) content = strings.TrimSpace(fmt.Sprintf(fmtStr, contentNoNL)) } } return content } func shortPostDescription(content string) string { maxLen := 140 fmtStr := "%s" truncation := 0 if utf8.RuneCountInString(content) > maxLen { // Post is longer than the max description, so let's show a better description fmtStr = "%s..." truncation = 3 } return strings.TrimSpace(fmt.Sprintf(fmtStr, strings.Replace(stringmanip.Substring(content, 0, maxLen-truncation), "\n", " ", -1))) } func handleRenderMarkdown(app *App, w http.ResponseWriter, r *http.Request) error { if !IsJSON(r) { return impart.HTTPError{Status: http.StatusUnsupportedMediaType, Message: "Markdown API only supports JSON requests"} } in := struct { CollectionURL string `json:"collection_url"` RawBody string `json:"raw_body"` }{} decoder := json.NewDecoder(r.Body) err := decoder.Decode(&in) if err != nil { log.Error("Couldn't parse markdown JSON request: %v", err) return ErrBadJSON } - body := in.RawBody - if in.CollectionURL != "" { - body = strings.Replace(body, shortCodeMore, `Read more...`, 1) - body = alterShortCodeEmailSubForm(body, "example", "slug", true) - } - rendered := applyMarkdown([]byte(in.RawBody), in.CollectionURL, app.cfg) out := struct { Body string `json:"body"` }{ - Body: rendered, + Body: applyMarkdown([]byte(in.RawBody), in.CollectionURL, app.cfg), } return impart.WriteSuccess(w, out, http.StatusOK) } - -func alterShortCodeEmailSubForm(postContent, alias, slug string, isDisabled bool) string { - subURL := `/api/collections/` + alias + `/email/subscribe` - if isDisabled { - subURL = "" - } - formHTML := `
` - return strings.Replace(postContent, shortCodeEmailSub, formHTML, -1) -} diff --git a/posts.go b/posts.go index 2fad164..5c3ab19 100644 --- a/posts.go +++ b/posts.go @@ -1,1770 +1,1770 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "database/sql" "encoding/json" "fmt" "html/template" "net/http" "net/url" "regexp" "strings" "time" "github.com/gorilla/mux" "github.com/gosimple/slug" "github.com/guregu/null" "github.com/guregu/null/zero" "github.com/kylemcc/twitter-text-go/extract" "github.com/microcosm-cc/bluemonday" stripmd "github.com/writeas/go-strip-markdown/v2" "github.com/writeas/impart" "github.com/writeas/monday" "github.com/writeas/web-core/activitystreams" "github.com/writeas/web-core/bots" "github.com/writeas/web-core/converter" "github.com/writeas/web-core/i18n" "github.com/writeas/web-core/log" "github.com/writeas/web-core/tags" "github.com/writefreely/writefreely/page" "github.com/writefreely/writefreely/parse" + "github.com/writefreely/writefreely/spam" ) const ( // Post ID length bounds minIDLen = 10 maxIDLen = 10 userPostIDLen = 10 postIDLen = 10 - postMetaDateFormat = "2006-01-02T15:04:05Z" + postMetaDateFormat = "2006-01-02 15:04:05" ) type PostType string const ( postArch PostType = "archive" - shortCodeMore = "" - shortCodePaid = "" - shortCodeNoSig = "" - shortCodeEmailSub = "" + shortCodeMore = "" + shortCodePaid = "" + shortCodeNoSig = "" ) type ( AnonymousPost struct { ID string Content string HTMLContent template.HTML Font string Language string Direction string Title string GenTitle string Description string Author string Views int64 Images []string IsPlainText bool IsCode bool IsLinkable bool } AuthenticatedPost struct { ID string `json:"id" schema:"id"` Web bool `json:"web" schema:"web"` *SubmittedPost } // SubmittedPost represents a post supplied by a client for publishing or // updating. Since Title and Content can be updated to "", they are // pointers that can be easily tested to detect changes. SubmittedPost struct { Slug *string `json:"slug" schema:"slug"` Title *string `json:"title" schema:"title"` Content *string `json:"body" schema:"body"` Font string `json:"font" schema:"font"` IsRTL converter.NullJSONBool `json:"rtl" schema:"rtl"` Language converter.NullJSONString `json:"lang" schema:"lang"` Created *string `json:"created" schema:"created"` } // Post represents a post as found in the database. Post struct { ID string `db:"id" json:"id"` Slug null.String `db:"slug" json:"slug,omitempty"` Font string `db:"text_appearance" json:"appearance"` Language zero.String `db:"language" json:"language"` RTL zero.Bool `db:"rtl" json:"rtl"` Privacy int64 `db:"privacy" json:"-"` OwnerID null.Int `db:"owner_id" json:"-"` CollectionID null.Int `db:"collection_id" json:"-"` PinnedPosition null.Int `db:"pinned_position" json:"-"` Created time.Time `db:"created" json:"created"` Updated time.Time `db:"updated" json:"updated"` ViewCount int64 `db:"view_count" json:"-"` LikeCount int64 `db:"like_count" json:"likes"` Title zero.String `db:"title" json:"title"` HTMLTitle template.HTML `db:"title" json:"-"` Content string `db:"content" json:"body"` HTMLContent template.HTML `db:"content" json:"-"` HTMLExcerpt template.HTML `db:"content" json:"-"` Tags []string `json:"tags"` Images []string `json:"images,omitempty"` IsPaid bool `json:"paid"` OwnerName string `json:"owner,omitempty"` } // PublicPost holds properties for a publicly returned post, i.e. a post in // a context where the viewer may not be the owner. As such, sensitive // metadata for the post is hidden and properties supporting the display of // the post are added. PublicPost struct { *Post IsSubdomain bool `json:"-"` IsTopLevel bool `json:"-"` DisplayDate string `json:"-"` Views int64 `json:"views"` Likes int64 `json:"likes"` Owner *PublicUser `json:"-"` IsOwner bool `json:"-"` URL string `json:"url,omitempty"` Collection *CollectionObj `json:"collection,omitempty"` } CollectionPostPage struct { *PublicPost page.StaticPage IsOwner bool IsPinned bool IsCustomDomain bool Monetization string Verification string FediverseAuthor string PinnedPosts *[]PublicPost IsFound bool IsAdmin bool CanInvite bool Silenced bool // Helper field for Chorus mode CollAlias string } RawPost struct { Id, Slug string Title string Content string Views int64 Font string Created time.Time Updated time.Time IsRTL sql.NullBool Language sql.NullString OwnerID int64 CollectionID sql.NullInt64 Found bool Gone bool } AnonymousAuthPost struct { ID string `json:"id"` Token string `json:"token"` } ClaimPostRequest struct { *AnonymousAuthPost CollectionAlias string `json:"collection"` CreateCollection bool `json:"create_collection"` // Generated properties Slug string `json:"-"` } ClaimPostResult struct { ID string `json:"id,omitempty"` Code int `json:"code,omitempty"` ErrorMessage string `json:"error_msg,omitempty"` Post *PublicPost `json:"post,omitempty"` } ) func (p *Post) Direction() string { if p.RTL.Valid { if p.RTL.Bool { return "rtl" } return "ltr" } return "auto" } // DisplayTitle dynamically generates a title from the Post's contents if it // doesn't already have an explicit title. func (p *Post) DisplayTitle() string { if p.Title.String != "" { return p.Title.String } t := friendlyPostTitle(p.Content, p.ID) return t } -// PlainDisplayTitle strips away Markdown from the generated Post's title (if -// any), for use in places like RSS feeds and ActivityStreams objects, where -// the raw Markdown would be unwanted. +// PlainDisplayTitle dynamically generates a title from the Post's contents if it +// doesn't already have an explicit title. func (p *Post) PlainDisplayTitle() string { if t := stripmd.Strip(p.DisplayTitle()); t != "" { return t } return p.ID } // FormattedDisplayTitle dynamically generates a title from the Post's contents if it // doesn't already have an explicit title. func (p *Post) FormattedDisplayTitle() template.HTML { if p.HTMLTitle != "" { return p.HTMLTitle } return template.HTML(p.DisplayTitle()) } // Summary gives a shortened summary of the post based on the post's title, // especially for display in a longer list of posts. It extracts a summary for // posts in the Title\n\nBody format, returning nothing if the entire was short // enough that the extracted title == extracted summary. func (p Post) Summary() string { if p.Content == "" { return "" } p.Content = stripHTMLWithoutEscaping(p.Content) // and Markdown p.Content = stripmd.StripOptions(p.Content, stripmd.Options{SkipImages: true}) title := p.Title.String var desc string if title == "" { // No title, so generate one title = friendlyPostTitle(p.Content, p.ID) desc = postDescription(p.Content, title, p.ID) if desc == title { return "" } return desc } return shortPostDescription(p.Content) } func (p Post) SummaryHTML() template.HTML { return template.HTML(p.Summary()) } // Excerpt shows any text that comes before a (more) tag. // TODO: use HTMLExcerpt in templates instead of this method func (p *Post) Excerpt() template.HTML { return p.HTMLExcerpt } func (p *Post) CreatedDate() string { return p.Created.Format("2006-01-02") } func (p *Post) Created8601() string { return p.Created.Format("2006-01-02T15:04:05Z") } func (p *Post) IsScheduled() bool { return p.Created.After(time.Now()) } func (p *Post) HasTag(tag string) bool { // Regexp looks for tag and has a non-capturing group at the end looking // for the end of the word. // Assisted by: https://stackoverflow.com/a/35192941/1549194 hasTag, _ := regexp.MatchString("#"+tag+`(?:[[:punct:]]|\s|\z)`, p.Content) return hasTag } func (p *Post) HasTitleLink() bool { if p.Title.String == "" { return false } hasLink, _ := regexp.MatchString(`([^!]+|^)\[.+\]\(.+\)`, p.Title.String) return hasLink } func (c CollectionPostPage) DisplayMonetization() string { if c.Collection == nil { log.Info("CollectionPostPage.DisplayMonetization: c.Collection is nil") return "" } return displayMonetization(c.Monetization, c.Collection.Alias) } func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) friendlyID := vars["post"] // NOTE: until this is done better, be sure to keep this in parity with // isRaw() and viewCollectionPost() isJSON := strings.HasSuffix(friendlyID, ".json") isXML := strings.HasSuffix(friendlyID, ".xml") isCSS := strings.HasSuffix(friendlyID, ".css") isMarkdown := strings.HasSuffix(friendlyID, ".md") isRaw := strings.HasSuffix(friendlyID, ".txt") || isJSON || isXML || isCSS || isMarkdown // Display reserved page if that is requested resource if t, ok := pages[r.URL.Path[1:]+".tmpl"]; ok { return handleTemplatedPage(app, w, r, t) } else if r.URL.Path == "/sitemap.xml" && !app.cfg.App.SingleUser { return impart.HTTPError{Status: http.StatusNotFound, Message: "Page not found."} } else if (strings.Contains(r.URL.Path, ".") && !isRaw && !isMarkdown) || r.URL.Path == "/robots.txt" || r.URL.Path == "/manifest.json" { // Serve static file app.shttp.ServeHTTP(w, r) return nil } // Display collection if this is a collection c, _ := app.db.GetCollection(friendlyID) if c != nil { return impart.HTTPError{http.StatusMovedPermanently, fmt.Sprintf("/%s/", friendlyID)} } // Normalize the URL, redirecting user to consistent post URL if friendlyID != strings.ToLower(friendlyID) { return impart.HTTPError{http.StatusMovedPermanently, fmt.Sprintf("/%s", strings.ToLower(friendlyID))} } ext := "" if isRaw { parts := strings.Split(friendlyID, ".") friendlyID = parts[0] if len(parts) > 1 { ext = "." + parts[1] } } var ownerID sql.NullInt64 var collectionID sql.NullInt64 var title string var content string var font string var language []byte var rtl []byte var views int64 var post *AnonymousPost var found bool var gone bool fixedID := slug.Make(friendlyID) if fixedID != friendlyID { return impart.HTTPError{http.StatusFound, fmt.Sprintf("/%s%s", fixedID, ext)} } err := app.db.QueryRow("SELECT owner_id, collection_id, title, content, text_appearance, view_count, language, rtl FROM posts WHERE id = ?", friendlyID).Scan(&ownerID, &collectionID, &title, &content, &font, &views, &language, &rtl) switch { case err == sql.ErrNoRows: found = false // Output the error in the correct format if isJSON { content = "{\"error\": \"Post not found.\"}" } else if isRaw { content = "Post not found." } else { return ErrPostNotFound } case err != nil: found = false log.Error("Post loading err: %s\n", err) return ErrInternalGeneral default: found = true var d string if len(rtl) == 0 { d = "auto" } else if rtl[0] == 49 { // TODO: find a cleaner way to get this (possibly NULL) value d = "rtl" } else { d = "ltr" } generatedTitle := friendlyPostTitle(content, friendlyID) sanitizedContent := content if font != "code" { sanitizedContent = template.HTMLEscapeString(content) } var desc string if title == "" { desc = postDescription(content, title, friendlyID) } else { desc = shortPostDescription(content) } post = &AnonymousPost{ ID: friendlyID, Content: sanitizedContent, Title: title, GenTitle: generatedTitle, Description: desc, Author: "", Font: font, IsPlainText: isRaw, IsCode: font == "code", IsLinkable: font != "code", Views: views, Language: string(language), Direction: d, } if !isRaw { post.HTMLContent = template.HTML(applyMarkdown([]byte(content), "", app.cfg)) post.Images = extractImages(post.Content) } } var silenced bool if found { silenced, err = app.db.IsUserSilenced(ownerID.Int64) if err != nil { log.Error("view post: %v", err) } } var protectDraft bool if found && collectionID.Valid { collection, err := app.db.GetCollectionByID(collectionID.Int64) if err != nil { log.Error("view post: %v", err) } protectDraft = collection.IsPrivate() || collection.IsProtected() } // Check if post has been unpublished if title == "" && content == "" { gone = true if isJSON { content = "{\"error\": \"Post was unpublished.\"}" } else if isCSS { content = "" } else if isRaw { content = "Post was unpublished." } else { return ErrPostUnpublished } } var u = &User{} if isRaw { contentType := "text/plain" if isJSON { contentType = "application/json" } else if isCSS { contentType = "text/css" } else if isXML { contentType = "application/xml" } else if isMarkdown { contentType = "text/markdown" } w.Header().Set("Content-Type", fmt.Sprintf("%s; charset=utf-8", contentType)) if isMarkdown && post.Title != "" { fmt.Fprintf(w, "%s\n", post.Title) for i := 1; i <= len(post.Title); i++ { fmt.Fprintf(w, "=") } fmt.Fprintf(w, "\n\n") } fmt.Fprint(w, content) if !found { return ErrPostNotFound } else if gone { return ErrPostUnpublished } } else { var err error page := struct { *AnonymousPost page.StaticPage Username string IsOwner bool SiteURL string Silenced bool }{ AnonymousPost: post, StaticPage: pageForReq(app, r), SiteURL: app.cfg.App.Host, } if u = getUserSession(app, r); u != nil { page.Username = u.Username page.IsOwner = ownerID.Valid && ownerID.Int64 == u.ID } if !page.IsOwner && silenced { return ErrPostNotFound } if !page.IsOwner && protectDraft { return ErrPostNotFound } page.Silenced = silenced err = templates["post"].ExecuteTemplate(w, "post", page) if err != nil { log.Error("Post template execute error: %v", err) } } go func() { if u != nil && ownerID.Valid && ownerID.Int64 == u.ID { // Post is owned by someone; skip view increment since that person is viewing this post. return } // Update stats for non-raw post views if !isRaw && r.Method != "HEAD" && !bots.IsBot(r.UserAgent()) { _, err := app.db.Exec("UPDATE posts SET view_count = view_count + 1 WHERE id = ?", friendlyID) if err != nil { log.Error("Unable to update posts count: %v", err) } } }() return nil } // API v2 funcs // newPost creates a new post with or without an owning Collection. // // Endpoints: // - /posts // - /posts?collection={alias} // - ? /collections/{alias}/posts func newPost(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r) vars := mux.Vars(r) collAlias := vars["alias"] if collAlias == "" { collAlias = r.FormValue("collection") } accessToken := r.Header.Get("Authorization") if accessToken == "" { // TODO: remove this accessToken = r.FormValue("access_token") } // FIXME: determine web submission with Content-Type header var u *User var userID int64 = -1 var username string if accessToken == "" { u = getUserSession(app, r) if u != nil { userID = u.ID username = u.Username } } else { userID = app.db.GetUserID(accessToken) } silenced, err := app.db.IsUserSilenced(userID) if err != nil { log.Error("new post: %v", err) } if silenced { return ErrUserSilenced } if userID == -1 { return ErrNotLoggedIn } if accessToken == "" && u == nil && collAlias != "" { return impart.HTTPError{http.StatusBadRequest, "Parameter `access_token` required."} } // Get post data var p *SubmittedPost if reqJSON { decoder := json.NewDecoder(r.Body) err = decoder.Decode(&p) if err != nil { log.Error("Couldn't parse new post JSON request: %v\n", err) return ErrBadJSON } if p.Title == nil { t := "" p.Title = &t } if strings.TrimSpace(*(p.Title)) == "" && (p.Content == nil || strings.TrimSpace(*(p.Content)) == "") { return ErrNoPublishableContent } if p.Content == nil { c := "" p.Content = &c } } else { post := r.FormValue("body") appearance := r.FormValue("font") title := r.FormValue("title") rtlValue := r.FormValue("rtl") langValue := r.FormValue("lang") if strings.TrimSpace(post) == "" { return ErrNoPublishableContent } var isRTL, rtlValid bool if rtlValue == "auto" && langValue != "" { isRTL = i18n.LangIsRTL(langValue) rtlValid = true } else { isRTL = rtlValue == "true" rtlValid = rtlValue != "" && langValue != "" } // Create a new post p = &SubmittedPost{ Title: &title, Content: &post, Font: appearance, IsRTL: converter.NullJSONBool{sql.NullBool{Bool: isRTL, Valid: rtlValid}}, Language: converter.NullJSONString{sql.NullString{String: langValue, Valid: langValue != ""}}, } } if !p.isFontValid() { p.Font = "norm" } var newPost *PublicPost = &PublicPost{} var coll *Collection if accessToken != "" { newPost, err = app.db.CreateOwnedPost(p, accessToken, collAlias, app.cfg.App.Host) } else { //return ErrNotLoggedIn // TODO: verify user is logged in var collID int64 if collAlias != "" { coll, err = app.db.GetCollection(collAlias) if err != nil { return err } coll.hostName = app.cfg.App.Host if coll.OwnerID != u.ID { return ErrForbiddenCollection } collID = coll.ID } // TODO: return PublicPost from createPost newPost.Post, err = app.db.CreatePost(userID, collID, p) } if err != nil { return err } if coll != nil { coll.ForPublic() newPost.Collection = &CollectionObj{Collection: *coll} } newPost.extractData() newPost.OwnerName = username newPost.URL = newPost.CanonicalURL(app.cfg.App.Host) // Write success now response := impart.WriteSuccess(w, newPost, http.StatusCreated) if newPost.Collection != nil { if !app.cfg.App.Private && app.cfg.App.Federation && !newPost.Created.After(time.Now()) { go federatePost(app, newPost, newPost.Collection.ID, false) } if app.cfg.Email.Enabled() && newPost.Collection.EmailSubsEnabled() { go app.db.InsertJob(&PostJob{ PostID: newPost.ID, Action: "email", Delay: emailSendDelay, }) } } return response } func existingPost(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r) vars := mux.Vars(r) postID := vars["post"] p := AuthenticatedPost{ID: postID} var err error if reqJSON { // Decode JSON request decoder := json.NewDecoder(r.Body) err = decoder.Decode(&p) if err != nil { log.Error("Couldn't parse post update JSON request: %v\n", err) return ErrBadJSON } } else { err = r.ParseForm() if err != nil { log.Error("Couldn't parse post update form request: %v\n", err) return ErrBadFormData } // Can't decode to a nil SubmittedPost property, so create instance now p.SubmittedPost = &SubmittedPost{} err = app.formDecoder.Decode(&p, r.PostForm) if err != nil { log.Error("Couldn't decode post update form request: %v\n", err) return ErrBadFormData } } if p.Web { p.IsRTL.Valid = true } if p.SubmittedPost == nil { return ErrPostNoUpdatableVals } // Ensure an access token was given accessToken := r.Header.Get("Authorization") // Get user's cookie session if there's no token var u *User //var username string if accessToken == "" { u = getUserSession(app, r) if u != nil { //username = u.Username } } if u == nil && accessToken == "" { return ErrNoAccessToken } // Get user ID from current session or given access token, if one was given. var userID int64 if u != nil { userID = u.ID } else if accessToken != "" { userID, err = AuthenticateUser(app.db, accessToken) if err != nil { return err } } silenced, err := app.db.IsUserSilenced(userID) if err != nil { log.Error("existing post: %v", err) } if silenced { return ErrUserSilenced } // Modify post struct p.ID = postID err = app.db.UpdateOwnedPost(&p, userID) if err != nil { if reqJSON { return err } if err, ok := err.(impart.HTTPError); ok { addSessionFlash(app, w, r, err.Message, nil) } else { addSessionFlash(app, w, r, err.Error(), nil) } } var pRes *PublicPost pRes, err = app.db.GetPost(p.ID, 0) if reqJSON { if err != nil { return err } pRes.extractData() } if pRes.CollectionID.Valid { coll, err := app.db.GetCollectionBy("id = ?", pRes.CollectionID.Int64) if err == nil && !app.cfg.App.Private && app.cfg.App.Federation { coll.hostName = app.cfg.App.Host pRes.Collection = &CollectionObj{Collection: *coll} go federatePost(app, pRes, pRes.Collection.ID, true) } } // Write success now if reqJSON { return impart.WriteSuccess(w, pRes, http.StatusOK) } addSessionFlash(app, w, r, "Changes saved.", nil) collectionAlias := vars["alias"] redirect := "/" + postID + "/meta" if collectionAlias != "" { collPre := "/" + collectionAlias if app.cfg.App.SingleUser { collPre = "" } redirect = collPre + "/" + pRes.Slug.String + "/edit/meta" } else { if app.cfg.App.SingleUser { redirect = "/d" + redirect } } + redirect = app.cfg.App.PrefixPath(redirect) w.Header().Set("Location", redirect) w.WriteHeader(http.StatusFound) return nil } func deletePost(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) friendlyID := vars["post"] editToken := r.FormValue("token") var ownerID int64 var u *User accessToken := r.Header.Get("Authorization") if accessToken == "" && editToken == "" { u = getUserSession(app, r) if u == nil { return ErrNoAccessToken } } var res sql.Result var t *sql.Tx var err error var collID sql.NullInt64 var coll *Collection var pp *PublicPost if editToken != "" { // TODO: SELECT owner_id, as well, and return appropriate error if NULL instead of running two queries var dummy int64 err = app.db.QueryRow("SELECT 1 FROM posts WHERE id = ?", friendlyID).Scan(&dummy) switch { case err == sql.ErrNoRows: return impart.HTTPError{http.StatusNotFound, "Post not found."} } err = app.db.QueryRow("SELECT 1 FROM posts WHERE id = ? AND owner_id IS NULL", friendlyID).Scan(&dummy) switch { case err == sql.ErrNoRows: // Post already has an owner. This could provide a bad experience // for the user, but it's more important to ensure data isn't lost // unexpectedly. So prevent deletion via token. return impart.HTTPError{http.StatusConflict, "This post belongs to some user (hopefully yours). Please log in and delete it from that user's account."} } res, err = app.db.Exec("DELETE FROM posts WHERE id = ? AND modify_token = ? AND owner_id IS NULL", friendlyID, editToken) } else if accessToken != "" || u != nil { // Caller provided some way to authenticate; assume caller expects the // post to be deleted based on a specific post owner, thus we should // return corresponding errors. if accessToken != "" { ownerID = app.db.GetUserID(accessToken) if ownerID == -1 { return ErrBadAccessToken } } else { ownerID = u.ID } // TODO: don't make two queries var realOwnerID sql.NullInt64 err = app.db.QueryRow("SELECT collection_id, owner_id FROM posts WHERE id = ?", friendlyID).Scan(&collID, &realOwnerID) if err != nil { return err } if !collID.Valid { // There's no collection; simply delete the post res, err = app.db.Exec("DELETE FROM posts WHERE id = ? AND owner_id = ?", friendlyID, ownerID) } else { // Post belongs to a collection; do any additional clean up coll, err = app.db.GetCollectionBy("id = ?", collID.Int64) if err != nil { log.Error("Unable to get collection: %v", err) return err } if app.cfg.App.Federation { // First fetch full post for federation pp, err = app.db.GetOwnedPost(friendlyID, ownerID) if err != nil { log.Error("Unable to get owned post: %v", err) return err } collObj := &CollectionObj{Collection: *coll} pp.Collection = collObj } t, err = app.db.Begin() if err != nil { log.Error("No begin: %v", err) return err } res, err = t.Exec("DELETE FROM posts WHERE id = ? AND owner_id = ?", friendlyID, ownerID) } } else { return impart.HTTPError{http.StatusBadRequest, "No authenticated user or post token given."} } if err != nil { return err } affected, err := res.RowsAffected() if err != nil { if t != nil { t.Rollback() log.Error("Rows affected err! Rolling back") } return err } else if affected == 0 { if t != nil { t.Rollback() log.Error("No rows affected! Rolling back") } return impart.HTTPError{http.StatusForbidden, "Post not found, or you're not the owner."} } if t != nil { t.Commit() } if coll != nil && !app.cfg.App.Private && app.cfg.App.Federation { go deleteFederatedPost(app, pp, collID.Int64) } return impart.HTTPError{Status: http.StatusNoContent} } // addPost associates a post with the authenticated user. func addPost(app *App, w http.ResponseWriter, r *http.Request) error { var ownerID int64 // Authenticate user at := r.Header.Get("Authorization") if at != "" { ownerID = app.db.GetUserID(at) if ownerID == -1 { return ErrBadAccessToken } } else { u := getUserSession(app, r) if u == nil { return ErrNotLoggedIn } ownerID = u.ID } silenced, err := app.db.IsUserSilenced(ownerID) if err != nil { log.Error("add post: %v", err) } if silenced { return ErrUserSilenced } // Parse claimed posts in format: // [{"id": "...", "token": "..."}] var claims *[]ClaimPostRequest decoder := json.NewDecoder(r.Body) err = decoder.Decode(&claims) if err != nil { return ErrBadJSONArray } vars := mux.Vars(r) collAlias := vars["alias"] // Update all given posts res, err := app.db.ClaimPosts(app.cfg, ownerID, collAlias, claims) if err != nil { return err } for _, pRes := range *res { if pRes.Code != http.StatusOK { continue } if !app.cfg.App.Private && app.cfg.App.Federation { if !pRes.Post.Created.After(time.Now()) { pRes.Post.Collection.hostName = app.cfg.App.Host go federatePost(app, pRes.Post, pRes.Post.Collection.ID, false) } } if app.cfg.Email.Enabled() && pRes.Post.Collection.EmailSubsEnabled() { go app.db.InsertJob(&PostJob{ PostID: pRes.Post.ID, Action: "email", Delay: emailSendDelay, }) } } return impart.WriteSuccess(w, res, http.StatusOK) } func dispersePost(app *App, w http.ResponseWriter, r *http.Request) error { var ownerID int64 // Authenticate user at := r.Header.Get("Authorization") if at != "" { ownerID = app.db.GetUserID(at) if ownerID == -1 { return ErrBadAccessToken } } else { u := getUserSession(app, r) if u == nil { return ErrNotLoggedIn } ownerID = u.ID } // Parse posts in format: // ["..."] var postIDs []string decoder := json.NewDecoder(r.Body) err := decoder.Decode(&postIDs) if err != nil { return ErrBadJSONArray } // Update all given posts res, err := app.db.DispersePosts(ownerID, postIDs) if err != nil { return err } return impart.WriteSuccess(w, res, http.StatusOK) } type ( PinPostResult struct { ID string `json:"id,omitempty"` Code int `json:"code,omitempty"` ErrorMessage string `json:"error_msg,omitempty"` } ) // pinPost pins a post to a blog func pinPost(app *App, w http.ResponseWriter, r *http.Request) error { var userID int64 // Authenticate user at := r.Header.Get("Authorization") if at != "" { userID = app.db.GetUserID(at) if userID == -1 { return ErrBadAccessToken } } else { u := getUserSession(app, r) if u == nil { return ErrNotLoggedIn } userID = u.ID } silenced, err := app.db.IsUserSilenced(userID) if err != nil { log.Error("pin post: %v", err) } if silenced { return ErrUserSilenced } // Parse request var posts []struct { ID string `json:"id"` Position int64 `json:"position"` } decoder := json.NewDecoder(r.Body) err = decoder.Decode(&posts) if err != nil { return ErrBadJSONArray } // Validate data vars := mux.Vars(r) collAlias := vars["alias"] coll, err := app.db.GetCollection(collAlias) if err != nil { return err } if coll.OwnerID != userID { return ErrForbiddenCollection } // Do (un)pinning isPinning := r.URL.Path[strings.LastIndex(r.URL.Path, "/"):] == "/pin" res := []PinPostResult{} for _, p := range posts { err = app.db.UpdatePostPinState(isPinning, p.ID, coll.ID, userID, p.Position) ppr := PinPostResult{ID: p.ID} if err != nil { ppr.Code = http.StatusInternalServerError // TODO: set error message } else { ppr.Code = http.StatusOK } res = append(res, ppr) } return impart.WriteSuccess(w, res, http.StatusOK) } func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { var collID int64 var coll *Collection var err error vars := mux.Vars(r) if collAlias := vars["alias"]; collAlias != "" { // Fetch collection information, since an alias is provided coll, err = app.db.GetCollection(collAlias) if err != nil { return err } collID = coll.ID } p, err := app.db.GetPost(vars["post"], collID) if err != nil { return err } if coll == nil && p.CollectionID.Valid { // Collection post is getting fetched by post ID, not coll alias + post slug, so get coll info now. coll, err = app.db.GetCollectionByID(p.CollectionID.Int64) if err != nil { return err } } if coll != nil { coll.hostName = app.cfg.App.Host _, err = apiCheckCollectionPermissions(app, r, coll) if err != nil { return err } } silenced, err := app.db.IsUserSilenced(p.OwnerID.Int64) if err != nil { log.Error("fetch post: %v", err) } if silenced { return ErrPostNotFound } p.extractData() if IsActivityPubRequest(r) { if coll == nil { // This is a draft post; 404 for now // TODO: return ActivityObject return impart.HTTPError{http.StatusNotFound, ""} } p.Collection = &CollectionObj{Collection: *coll} po := p.ActivityObject(app) po.Context = []interface{}{activitystreams.Namespace} setCacheControl(w, apCacheTime) return impart.RenderActivityJSON(w, po, http.StatusOK) } return impart.WriteSuccess(w, p, http.StatusOK) } func fetchPostProperty(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) p, err := app.db.GetPostProperty(vars["post"], 0, vars["property"]) if err != nil { return err } return impart.WriteSuccess(w, p, http.StatusOK) } func (p *Post) processPost() PublicPost { res := &PublicPost{Post: p, Views: 0} res.Views = p.ViewCount res.Likes = p.LikeCount // TODO: move to own function loc := monday.FuzzyLocale(p.Language.String) res.DisplayDate = monday.Format(p.Created, monday.LongFormatsByLocale[loc], loc) return *res } func (p *PublicPost) CanonicalURL(hostName string) string { if p.Collection == nil || p.Collection.Alias == "" { return hostName + "/" + p.ID + ".md" } return p.Collection.CanonicalURL() + p.Slug.String } func (pp *PublicPost) DisplayCanonicalURL() string { us := pp.CanonicalURL(pp.Collection.hostName) u, err := url.Parse(us) if err != nil { return us } return u.Hostname() + u.Path } func (p *PublicPost) ActivityObject(app *App) *activitystreams.Object { cfg := app.cfg var o *activitystreams.Object if cfg.App.NotesOnly || strings.Index(p.Content, "\n\n") == -1 { o = activitystreams.NewNoteObject() } else { o = activitystreams.NewArticleObject() } o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID o.Published = p.Created o.URL = p.CanonicalURL(cfg.App.Host) o.AttributedTo = p.Collection.FederatedAccount() o.CC = []string{ p.Collection.FederatedAccount() + "/followers", } - o.Name = p.PlainDisplayTitle() + o.Name = p.DisplayTitle() p.augmentContent() if p.HTMLContent == template.HTML("") { p.formatContent(cfg, false, false) p.augmentReadingDestination() } o.Content = string(p.HTMLContent) if o.Type == "Note" && p.Title.String != "" { // Render the explicitly-set title inside the Note, since Mastodon (at least) doesn't show the `name` // property on Notes. o.Content = "

" + applyBasicMarkdown([]byte(p.DisplayTitle())) + "

\n\n" + o.Content } if p.Language.Valid { o.ContentMap = map[string]string{ p.Language.String: string(p.HTMLContent), } } if len(p.Tags) == 0 { o.Tag = []activitystreams.Tag{} } else { var tagBaseURL string if isSingleUser { tagBaseURL = p.Collection.CanonicalURL() + "tag:" } else { if cfg.App.Chorus { tagBaseURL = fmt.Sprintf("%s/read/t/", p.Collection.hostName) } else { tagBaseURL = fmt.Sprintf("%s/%s/tag:", p.Collection.hostName, p.Collection.Alias) } } for _, t := range p.Tags { o.Tag = append(o.Tag, activitystreams.Tag{ Type: activitystreams.TagHashtag, HRef: tagBaseURL + t, Name: "#" + t, }) } } if len(p.Images) > 0 { for _, i := range p.Images { o.Attachment = append(o.Attachment, activitystreams.NewImageAttachment(i)) } } // Find mentioned users mentionedUsers := make(map[string]string) stripper := bluemonday.StrictPolicy() content := stripper.Sanitize(p.Content) mentions := mentionReg.FindAllString(content, -1) for _, handle := range mentions { actorIRI, err := app.db.GetProfilePageFromHandle(app, handle) - if err != nil || actorIRI == "" { + if err != nil { log.Info("Couldn't find user '%s' locally or remotely", handle) continue } mentionedUsers[handle] = actorIRI } for handle, iri := range mentionedUsers { o.CC = append(o.CC, iri) o.Tag = append(o.Tag, activitystreams.Tag{Type: "Mention", HRef: iri, Name: handle}) } // Add shortened Note as the `preview` property if this is an Article if o.Type == "Article" { o.Preview = p.PreviewObject(app, o) o.Summary = &o.Preview.Content } return o } // PreviewObject returns an activitystreams.Object that can be used as an Article's `preview` property. func (p *PublicPost) PreviewObject(app *App, art *activitystreams.Object) *activitystreams.Object { o := activitystreams.NewNoteObject() o.To = nil o.ID = art.ID o.URL = art.URL o.Published = art.Published o.Updated = art.Updated o.Tag = art.Tag o.Attachment = art.Attachment baseURL := p.Collection.CanonicalURL() // Try to truncate at user-defined excerpt, if exists exc := strings.Index(p.Content, shortCodeMore) if exc == -1 { // No excerpt; fall back to truncating at first paragraph exc = strings.Index(p.Content, "\n\n") } if exc > -1 { p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:exc]+" [...]"), baseURL, app.cfg)) } else { p.HTMLExcerpt = p.HTMLContent } o.Content = strings.TrimRight(string(p.Excerpt()), "\n") return o } // TODO: merge this into getSlugFromPost or phase it out func getSlug(title, lang string) string { return getSlugFromPost("", title, lang) } func getSlugFromPost(title, body, lang string) string { if title == "" { // Remove Markdown, so e.g. link URLs and image alt text don't make it into the slug body = strings.TrimSpace(stripmd.StripOptions(body, stripmd.Options{SkipImages: true})) title = postTitle(body, body) } title = parse.PostLede(title, false) // Truncate lede if needed title, _ = parse.TruncToWord(title, 80) var s string if lang != "" && len(lang) == 2 { s = slug.MakeLang(title, lang) } else { s = slug.Make(title) } // Transliteration may cause the slug to expand past the limit, so truncate again s, _ = parse.TruncToWord(s, 80) return strings.TrimFunc(s, func(r rune) bool { // TruncToWord doesn't respect words in a slug, since spaces are replaced // with hyphens. So remove any trailing hyphens. return r == '-' }) } // isFontValid returns whether or not the submitted post's appearance is valid. func (p *SubmittedPost) isFontValid() bool { validFonts := map[string]bool{ "norm": true, "sans": true, "mono": true, "wrap": true, "code": true, } _, valid := validFonts[p.Font] return valid } func getRawPost(app *App, friendlyID string) *RawPost { var content, font, title string var isRTL sql.NullBool var lang sql.NullString var ownerID sql.NullInt64 var created, updated time.Time err := app.db.QueryRow("SELECT title, content, text_appearance, language, rtl, created, updated, owner_id FROM posts WHERE id = ?", friendlyID).Scan(&title, &content, &font, &lang, &isRTL, &created, &updated, &ownerID) switch { case err == sql.ErrNoRows: return &RawPost{Content: "", Found: false, Gone: false} case err != nil: log.Error("Unable to fetch getRawPost: %s", err) return &RawPost{Content: "", Found: true, Gone: false} } return &RawPost{ Title: title, Content: content, Font: font, Created: created, Updated: updated, IsRTL: isRTL, Language: lang, OwnerID: ownerID.Int64, Found: true, Gone: content == "" && title == "", } } // TODO; return a Post! func getRawCollectionPost(app *App, slug, collAlias string) *RawPost { var id, title, content, font string var isRTL sql.NullBool var lang sql.NullString var created, updated time.Time var ownerID null.Int var views int64 var err error if app.cfg.App.SingleUser { err = app.db.QueryRow("SELECT id, title, content, text_appearance, language, rtl, view_count, created, updated, owner_id FROM posts WHERE slug = ? AND collection_id = 1", slug).Scan(&id, &title, &content, &font, &lang, &isRTL, &views, &created, &updated, &ownerID) } else { err = app.db.QueryRow("SELECT id, title, content, text_appearance, language, rtl, view_count, created, updated, owner_id FROM posts WHERE slug = ? AND collection_id = (SELECT id FROM collections WHERE alias = ?)", slug, collAlias).Scan(&id, &title, &content, &font, &lang, &isRTL, &views, &created, &updated, &ownerID) } switch { case err == sql.ErrNoRows: return &RawPost{Content: "", Found: false, Gone: false} case err != nil: log.Error("Unable to fetch getRawCollectionPost: %s", err) return &RawPost{Content: "", Found: true, Gone: false} } return &RawPost{ Id: id, Slug: slug, Title: title, Content: content, Font: font, Created: created, Updated: updated, IsRTL: isRTL, Language: lang, OwnerID: ownerID.Int64, Found: true, Gone: content == "" && title == "", Views: views, } } func isRaw(r *http.Request) bool { vars := mux.Vars(r) slug := vars["slug"] // NOTE: until this is done better, be sure to keep this in parity with // isRaw in viewCollectionPost() and handleViewPost() isJSON := strings.HasSuffix(slug, ".json") isXML := strings.HasSuffix(slug, ".xml") isMarkdown := strings.HasSuffix(slug, ".md") return strings.HasSuffix(slug, ".txt") || isJSON || isXML || isMarkdown } func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) slug := vars["slug"] // NOTE: until this is done better, be sure to keep this in parity with // isRaw() and handleViewPost() isJSON := strings.HasSuffix(slug, ".json") isXML := strings.HasSuffix(slug, ".xml") isMarkdown := strings.HasSuffix(slug, ".md") isRaw := strings.HasSuffix(slug, ".txt") || isJSON || isXML || isMarkdown cr := &collectionReq{} err := processCollectionRequest(cr, vars, w, r) if err != nil { return err } // Check for hellbanned users u, err := checkUserForCollection(app, cr, r, true) if err != nil { return err } // Normalize the URL, redirecting user to consistent post URL if slug != strings.ToLower(slug) { loc := fmt.Sprintf("/%s", strings.ToLower(slug)) if !app.cfg.App.SingleUser { loc = "/" + cr.alias + loc } return impart.HTTPError{http.StatusMovedPermanently, loc} } // Display collection if this is a collection var c *Collection if app.cfg.App.SingleUser { c, err = app.db.GetCollectionByID(1) } else { c, err = app.db.GetCollection(cr.alias) } if err != nil { if err, ok := err.(impart.HTTPError); ok { if err.Status == http.StatusNotFound { // Redirect if necessary newAlias := app.db.GetCollectionRedirect(cr.alias) if newAlias != "" { return impart.HTTPError{http.StatusFound, "/" + newAlias + "/" + slug} } } } return err } c.hostName = app.cfg.App.Host silenced, err := app.db.IsUserSilenced(c.OwnerID) if err != nil { log.Error("view collection post: %v", err) } // Check collection permissions if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) { return ErrPostNotFound } if c.IsProtected() && (u == nil || u.ID != c.OwnerID) { if silenced { return ErrPostNotFound } else if !isAuthorizedForCollection(app, c.Alias, r) { return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "/?g=" + slug} } } cr.isCollOwner = u != nil && c.OwnerID == u.ID if isRaw { slug = strings.Split(slug, ".")[0] } // Fetch extra data about the Collection // TODO: refactor out this logic, shared in collection.go:fetchCollection() coll := NewCollectionObj(c) owner, err := app.db.GetUserByID(coll.OwnerID) if err != nil { // Log the error and just continue log.Error("Error getting user for collection: %v", err) } else { coll.Owner = owner } postFound := true p, err := app.db.GetPost(slug, coll.ID) if err != nil { if err == ErrCollectionPageNotFound { postFound = false if slug == "feed" { // User tried to access blog feed without a trailing slash, and // there's no post with a slug "feed" return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "feed/"} } else if slug == "archive" { // User tried to access blog Archive without a trailing slash, and // there's no post with a slug "archive" return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "archive/"} } po := &Post{ Slug: null.NewString(slug, true), Font: "norm", Language: zero.NewString("en", true), RTL: zero.NewBool(false, true), Content: `

This page is missing.

Are you sure it was ever here?` + shortCodeNoSig, } pp := po.processPost() p = &pp } else { return err } } // Check if the authenticated user is the post owner p.IsOwner = u != nil && u.ID == p.OwnerID.Int64 p.Collection = coll p.IsTopLevel = app.cfg.App.SingleUser // Only allow a post owner or admin to view a post for silenced collections if silenced && !p.IsOwner && (u == nil || !u.IsAdmin()) { return ErrPostNotFound } // Check if post has been unpublished if p.Content == "" && p.Title.String == "" { return impart.HTTPError{http.StatusGone, "Post was unpublished."} } p.augmentContent() // Serve collection post if isRaw { contentType := "text/plain" if isJSON { contentType = "application/json" } else if isXML { contentType = "application/xml" } else if isMarkdown { contentType = "text/markdown" } w.Header().Set("Content-Type", fmt.Sprintf("%s; charset=utf-8", contentType)) if !postFound { w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "Post not found.") // TODO: return error instead, so status is correctly reflected in logs return nil } if isMarkdown && p.Title.String != "" { fmt.Fprintf(w, "# %s\n\n", p.Title.String) } fmt.Fprint(w, p.Content) } else if IsActivityPubRequest(r) { if !postFound { return ErrCollectionPageNotFound } p.extractData() ap := p.ActivityObject(app) ap.Context = []interface{}{activitystreams.Namespace} setCacheControl(w, apCacheTime) return impart.RenderActivityJSON(w, ap, http.StatusOK) } else { p.extractData() p.Content = strings.Replace(p.Content, "", "", 1) if app.cfg.Email.Enabled() && c.EmailSubsEnabled() { // TODO: indicate plan is inactive or subs disabled when OWNER is viewing their own post. if u != nil && u.IsEmailSubscriber(app, c.ID) { - p.Content = strings.Replace(p.Content, shortCodeEmailSub, `

You're subscribed to email updates. Unsubscribe.

`, -1) + p.Content = strings.Replace(p.Content, "", `

You're subscribed to email updates. Unsubscribe.

`, -1) } else { - p.Content = alterShortCodeEmailSubForm(p.Content, c.Alias, p.Slug.String, false) + p.Content = strings.Replace(p.Content, "", `
`, -1) } } p.Content = strings.Replace(p.Content, "<!--emailsub-->", "", 1) // TODO: move this to function p.formatContent(app.cfg, cr.isCollOwner, true) tp := CollectionPostPage{ PublicPost: p, StaticPage: pageForReq(app, r), IsOwner: cr.isCollOwner, IsCustomDomain: cr.isCustomDomain, IsFound: postFound, Silenced: silenced, CollAlias: c.Alias, } tp.IsAdmin = u != nil && u.IsAdmin() tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll, p.IsOwner) tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p) tp.Monetization = coll.Monetization tp.Verification = coll.Verification if tp.Verification != "" { // Fetch info for fediverse:creator tag ru, err := getRemoteUserFromURL(app, coll.Verification) if err != nil { if debugging { log.Info("showing rel=me tag, but no local handle for %s", coll.Verification) } } else { // Though we don't store handles with leading @, strip it here just in case tp.FediverseAuthor = "@" + strings.TrimLeft(ru.Handle, "@") } } if !postFound { w.WriteHeader(http.StatusNotFound) } postTmpl := "collection-post" if app.cfg.App.Chorus { postTmpl = "chorus-collection-post" } if err := templates[postTmpl].ExecuteTemplate(w, "post", tp); err != nil { log.Error("Error in %s template: %v", postTmpl, err) } } go func() { if p.OwnerID.Valid { // Post is owned by someone. Don't update stats if owner is viewing the post. if u != nil && p.OwnerID.Int64 == u.ID { return } } // Update stats for non-raw post views if !isRaw && r.Method != "HEAD" && !bots.IsBot(r.UserAgent()) { _, err := app.db.Exec("UPDATE posts SET view_count = view_count + 1 WHERE slug = ? AND collection_id = ?", slug, coll.ID) if err != nil { log.Error("Unable to update posts count: %v", err) } } }() return nil } // TODO: move this to utils after making it more generic func PostsContains(sl *[]PublicPost, s *PublicPost) bool { for _, e := range *sl { if e.ID == s.ID { return true } } return false } func (p *Post) extractData() { p.Tags = tags.Extract(p.Content) p.extractImages() } func (p *Post) IsSans() bool { return p.Font == "sans" } func (p *Post) IsMonospace() bool { return p.Font == "mono" } func (rp *RawPost) UserFacingCreated() string { return rp.Created.Format(postMetaDateFormat) } func (rp *RawPost) Created8601() string { return rp.Created.Format("2006-01-02T15:04:05Z") } func (rp *RawPost) Updated8601() string { if rp.Updated.IsZero() { return "" } return rp.Updated.Format("2006-01-02T15:04:05Z") } var imageURLRegex = regexp.MustCompile(`(?i)[^ ]+\.(gif|png|jpg|jpeg|avif|avifs|webp|jxl|image)$`) func (p *Post) extractImages() { p.Images = extractImages(p.Content) } func extractImages(content string) []string { matches := extract.ExtractUrls(content) urls := map[string]bool{} for i := range matches { uRaw := matches[i].Text // Parse the extracted text so we can examine the path u, err := url.Parse(uRaw) if err != nil { continue } // Ensure the path looks like it leads to an image file if !imageURLRegex.MatchString(u.Path) { continue } urls[uRaw] = true } resURLs := make([]string, 0) for k := range urls { resURLs = append(resURLs, k) } return resURLs } diff --git a/prose/markdownSerializer.js b/prose/markdownSerializer.js index f25e23e..685703c 100644 --- a/prose/markdownSerializer.js +++ b/prose/markdownSerializer.js @@ -1,142 +1,128 @@ import { MarkdownSerializer } from "prosemirror-markdown"; function backticksFor(node, side) { const ticks = /`+/g; let m; let len = 0; if (node.isText) while ((m = ticks.exec(node.text))) len = Math.max(len, m[0].length); let result = len > 0 && side > 0 ? " `" : "`"; for (let i = 0; i < len; i++) result += "`"; if (len > 0 && side < 0) result += " "; return result; } function isPlainURL(link, parent, index, side) { if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false; const content = parent.child(index + (side < 0 ? -1 : 0)); if ( !content.isText || content.text != link.attrs.href || content.marks[content.marks.length - 1] != link ) return false; if (index == (side < 0 ? 1 : parent.childCount - 1)) return true; const next = parent.child(index + (side < 0 ? -2 : 1)); return !link.isInSet(next.marks); } export const writeFreelyMarkdownSerializer = new MarkdownSerializer( { readmore(state, node) { state.write("\n"); state.closeBlock(node); }, - emailsub(state, node) { - state.write("\n"); - state.closeBlock(node); - }, - comment(state, node) { - state.write(""); - }, - html_block(state, node) { - state.write(node.attrs.content); - state.closeBlock(node); - }, - html_inline(state, node) { - state.write(node.attrs.content); - }, blockquote(state, node) { state.wrapBlock("> ", null, node, () => state.renderContent(node)); }, code_block(state, node) { state.write(`\`\`\`${node.attrs.params || ""}\n`); state.text(node.textContent, false); state.ensureNewLine(); state.write("```"); state.closeBlock(node); }, heading(state, node) { state.write(`${state.repeat("#", node.attrs.level)} `); state.renderInline(node); state.closeBlock(node); }, horizontal_rule: function horizontal_rule(state, node) { state.write(node.attrs.markup || "---"); state.closeBlock(node); }, bullet_list(state, node) { node.attrs.tight = true; state.renderList(node, " ", () => `${node.attrs.bullet || "*"} `); }, ordered_list(state, node) { const start = node.attrs.order || 1; const maxW = String(start + node.childCount - 1).length; const space = state.repeat(" ", maxW + 2); state.renderList(node, space, (i) => { const nStr = String(start + i); return `${state.repeat(" ", maxW - nStr.length) + nStr}. `; }); }, list_item(state, node) { state.renderContent(node); }, paragraph(state, node) { state.renderInline(node); state.closeBlock(node); }, image(state, node) { state.write( `![${state.esc(node.attrs.alt || "")}](${state.esc(node.attrs.src)}${ node.attrs.title ? ` ${state.quote(node.attrs.title)}` : "" })` ); }, hard_break(state, node, parent, index) { for (let i = index + 1; i < parent.childCount; i += 1) if (parent.child(i).type !== node.type) { state.write("\\\n"); return; } }, text(state, node) { state.text(node.text || ""); }, }, { em: { open: "_", close: "_", mixable: true, expelEnclosingWhitespace: true, }, strong: { open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true, }, link: { open(_state, mark, parent, index) { return isPlainURL(mark, parent, index, 1) ? "<" : "["; }, close(state, mark, parent, index) { return isPlainURL(mark, parent, index, -1) ? ">" : `](${state.esc(mark.attrs.href)}${ mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : "" })`; }, }, code: { open(_state, _mark, parent, index) { return backticksFor(parent.child(index), -1); }, close(_state, _mark, parent, index) { return backticksFor(parent.child(index - 1), 1); }, escape: false, }, } ); diff --git a/prose/menu.js b/prose/menu.js index 0e45217..c534f61 100644 --- a/prose/menu.js +++ b/prose/menu.js @@ -1,49 +1,32 @@ import { MenuItem } from "prosemirror-menu"; import { buildMenuItems } from "prosemirror-example-setup"; -import { NodeSelection } from "prosemirror-state"; import { writeFreelySchema } from "./schema"; function canInsert(state, nodeType, attrs) { let $from = state.selection.$from; for (let d = $from.depth; d >= 0; d--) { let index = $from.index(d); if ($from.node(d).canReplaceWith(index, index, nodeType, attrs)) return true; } return false; } const ReadMoreItem = new MenuItem({ label: "Read more", select: (state) => canInsert(state, writeFreelySchema.nodes.readmore), run(state, dispatch) { dispatch( state.tr.replaceSelectionWith(writeFreelySchema.nodes.readmore.create()) ); }, }); export const getMenu = () => { - const builtItems = buildMenuItems(writeFreelySchema); - const { toggleLink } = builtItems; - - const patchedLink = new MenuItem({ - ...toggleLink.spec, - select(state) { - if ( - state.selection instanceof NodeSelection && - state.selection.node.type === writeFreelySchema.nodes.comment - ) { - return false; - } - return toggleLink.spec.select ? toggleLink.spec.select(state) : true; - }, - }); - - const fullMenu = builtItems.fullMenu.map((group) => - group.map((item) => (item === toggleLink ? patchedLink : item)) - ); - - return [...fullMenu, [ReadMoreItem]]; + const menuContent = [ + ...buildMenuItems(writeFreelySchema).fullMenu, + [ReadMoreItem], + ]; + return menuContent; }; diff --git a/prose/package-lock.json b/prose/package-lock.json index e82ddc6..2225f3e 100644 --- a/prose/package-lock.json +++ b/prose/package-lock.json @@ -1,16280 +1,16295 @@ { "name": "prose", "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prose", "version": "1.0.0", "license": "ISC", "dependencies": { "babel-core": "^6.26.3", "babel-preset-es2015": "^6.24.1", "markdown-it": "^12.0.4", "prosemirror-example-setup": "github:writefreely/prosemirror-example-setup", "prosemirror-keymap": "^1.1.4", "prosemirror-markdown": "github:writefreely/prosemirror-markdown", "prosemirror-model": "^1.9.1", "prosemirror-state": "^1.3.2", "prosemirror-view": "^1.14.2", "webpack": "^4.42.0", "webpack-cli": "^3.3.11" }, "devDependencies": { "@babel/core": "^7.8.7", "@babel/preset-env": "^7.9.0", "babel-loader": "^8.0.6", "prettier": "^2.2.1" } }, "node_modules/@babel/code-frame": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", "dev": true, "dependencies": { "@babel/highlight": "^7.8.3" } }, "node_modules/@babel/compat-data": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz", "integrity": "sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ==", "dev": true, "dependencies": { "browserslist": "^4.12.0", "invariant": "^2.2.4", "semver": "^5.5.0" } }, "node_modules/@babel/core": { "version": "7.8.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.7.tgz", "integrity": "sha512-rBlqF3Yko9cynC5CCFy6+K/w2N+Sq/ff2BPy+Krp7rHlABIr5epbA7OxVeKoMHB39LZOp1UY5SuLjy6uWi35yA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.8.3", "@babel/generator": "^7.8.7", "@babel/helpers": "^7.8.4", "@babel/parser": "^7.8.7", "@babel/template": "^7.8.6", "@babel/traverse": "^7.8.6", "@babel/types": "^7.8.7", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.1", "json5": "^2.1.0", "lodash": "^4.17.13", "resolve": "^1.3.2", "semver": "^5.4.1", "source-map": "^0.5.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core/node_modules/debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "dependencies": { "ms": "^2.1.1" } }, "node_modules/@babel/core/node_modules/json5": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", "dev": true, "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" }, "engines": { "node": ">=6" } }, "node_modules/@babel/core/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "node_modules/@babel/generator": { "version": "7.8.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.7.tgz", "integrity": "sha512-DQwjiKJqH4C3qGiyQCAExJHoZssn49JTMJgZ8SANGgVFdkupcUhLOdkAeoC6kmHZCPfoDG5M0b6cFlSN5wW7Ew==", "dev": true, "dependencies": { "@babel/types": "^7.8.7", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" } }, "node_modules/@babel/generator/node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, "bin": { "jsesc": "bin/jsesc" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "dependencies": { "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-annotate-as-pure/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-annotate-as-pure/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", "dev": true, "dependencies": { "@babel/helper-explode-assignable-expression": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-compilation-targets": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz", "integrity": "sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ==", "dev": true, "dependencies": { "@babel/compat-data": "^7.10.4", "browserslist": "^4.12.0", "invariant": "^2.2.4", "levenary": "^1.1.1", "semver": "^5.5.0" } }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz", "integrity": "sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==", "dev": true, "dependencies": { "@babel/helper-function-name": "^7.10.4", "@babel/helper-member-expression-to-functions": "^7.10.5", "@babel/helper-optimise-call-expression": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-replace-supers": "^7.10.4", "@babel/helper-split-export-declaration": "^7.10.4" } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "dependencies": { "@babel/highlight": "^7.10.4" } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "dependencies": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "dependencies": { "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "dependencies": { "@babel/types": "^7.11.0" } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { "has-flag": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz", "integrity": "sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-regex": "^7.10.4", "regexpu-core": "^4.7.0" } }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true, "bin": { "jsesc": "bin/jsesc" } }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/regexpu-core": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", "integrity": "sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==", "dev": true, "dependencies": { "regenerate": "^1.4.0", "regenerate-unicode-properties": "^8.2.0", "regjsgen": "^0.5.1", "regjsparser": "^0.6.4", "unicode-match-property-ecmascript": "^1.0.4", "unicode-match-property-value-ecmascript": "^1.2.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/regjsgen": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", "dev": true }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/regjsparser": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", "dev": true, "dependencies": { "jsesc": "~0.5.0" }, "bin": { "regjsparser": "bin/parser" } }, "node_modules/@babel/helper-define-map": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", "dev": true, "dependencies": { "@babel/helper-function-name": "^7.10.4", "@babel/types": "^7.10.5", "lodash": "^4.17.19" } }, "node_modules/@babel/helper-define-map/node_modules/@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "dependencies": { "@babel/highlight": "^7.10.4" } }, "node_modules/@babel/helper-define-map/node_modules/@babel/helper-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "dependencies": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-define-map/node_modules/@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "dependencies": { "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-define-map/node_modules/@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "node_modules/@babel/helper-define-map/node_modules/@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/helper-define-map/node_modules/@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-define-map/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-define-map/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-define-map/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-define-map/node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "node_modules/@babel/helper-define-map/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-define-map/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { "has-flag": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-define-map/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-explode-assignable-expression": { "version": "7.11.4", "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz", "integrity": "sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ==", "dev": true, "dependencies": { "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-explode-assignable-expression/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-explode-assignable-expression/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-explode-assignable-expression/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-function-name": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", "dev": true, "dependencies": { "@babel/helper-get-function-arity": "^7.8.3", "@babel/template": "^7.8.3", "@babel/types": "^7.8.3" } }, "node_modules/@babel/helper-get-function-arity": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", "dev": true, "dependencies": { "@babel/types": "^7.8.3" } }, "node_modules/@babel/helper-hoist-variables": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", "dev": true, "dependencies": { "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-hoist-variables/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-hoist-variables/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", "dev": true, "dependencies": { "@babel/types": "^7.11.0" } }, "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-member-expression-to-functions/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-member-expression-to-functions/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-module-imports": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "dependencies": { "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-module-imports/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-module-imports/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-module-transforms": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@babel/helper-replace-supers": "^7.10.4", "@babel/helper-simple-access": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", "@babel/template": "^7.10.4", "@babel/types": "^7.11.0", "lodash": "^4.17.19" } }, "node_modules/@babel/helper-module-transforms/node_modules/@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "dependencies": { "@babel/highlight": "^7.10.4" } }, "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "dependencies": { "@babel/types": "^7.11.0" } }, "node_modules/@babel/helper-module-transforms/node_modules/@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "node_modules/@babel/helper-module-transforms/node_modules/@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/helper-module-transforms/node_modules/@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-module-transforms/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-module-transforms/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-module-transforms/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-module-transforms/node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "node_modules/@babel/helper-module-transforms/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-module-transforms/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { "has-flag": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-module-transforms/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", "dev": true, "dependencies": { "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-optimise-call-expression/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-optimise-call-expression/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-plugin-utils": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", "dev": true }, "node_modules/@babel/helper-regex": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz", "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==", "dev": true, "dependencies": { "lodash": "^4.17.19" } }, "node_modules/@babel/helper-regex/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-remap-async-to-generator": { "version": "7.11.4", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz", "integrity": "sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-wrap-function": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "dependencies": { "@babel/highlight": "^7.10.4" } }, "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-remap-async-to-generator/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-remap-async-to-generator/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-remap-async-to-generator/node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "node_modules/@babel/helper-remap-async-to-generator/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-remap-async-to-generator/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { "has-flag": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-remap-async-to-generator/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-replace-supers": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", "dev": true, "dependencies": { "@babel/helper-member-expression-to-functions": "^7.10.4", "@babel/helper-optimise-call-expression": "^7.10.4", "@babel/traverse": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-replace-supers/node_modules/@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "dependencies": { "@babel/highlight": "^7.10.4" } }, "node_modules/@babel/helper-replace-supers/node_modules/@babel/generator": { "version": "7.11.6", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", "dev": true, "dependencies": { "@babel/types": "^7.11.5", "jsesc": "^2.5.1", "source-map": "^0.5.0" } }, "node_modules/@babel/helper-replace-supers/node_modules/@babel/helper-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "dependencies": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-replace-supers/node_modules/@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "dependencies": { "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-replace-supers/node_modules/@babel/helper-split-export-declaration": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "dependencies": { "@babel/types": "^7.11.0" } }, "node_modules/@babel/helper-replace-supers/node_modules/@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "node_modules/@babel/helper-replace-supers/node_modules/@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/helper-replace-supers/node_modules/@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-replace-supers/node_modules/@babel/traverse": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.11.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", "@babel/parser": "^7.11.5", "@babel/types": "^7.11.5", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "node_modules/@babel/helper-replace-supers/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-replace-supers/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-replace-supers/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-replace-supers/node_modules/debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "dependencies": { "ms": "^2.1.1" } }, "node_modules/@babel/helper-replace-supers/node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-replace-supers/node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "node_modules/@babel/helper-replace-supers/node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, "bin": { "jsesc": "bin/jsesc" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-replace-supers/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-replace-supers/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "node_modules/@babel/helper-replace-supers/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { "has-flag": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-replace-supers/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-simple-access": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", "dev": true, "dependencies": { "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-simple-access/node_modules/@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "dependencies": { "@babel/highlight": "^7.10.4" } }, "node_modules/@babel/helper-simple-access/node_modules/@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "node_modules/@babel/helper-simple-access/node_modules/@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/helper-simple-access/node_modules/@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-simple-access/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-simple-access/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-simple-access/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-simple-access/node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "node_modules/@babel/helper-simple-access/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-simple-access/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { "has-flag": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-simple-access/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz", "integrity": "sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==", "dev": true, "dependencies": { "@babel/types": "^7.11.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", "dev": true, "dependencies": { "@babel/types": "^7.8.3" } }, "node_modules/@babel/helper-validator-identifier": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "node_modules/@babel/helper-wrap-function": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz", "integrity": "sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==", "dev": true, "dependencies": { "@babel/helper-function-name": "^7.10.4", "@babel/template": "^7.10.4", "@babel/traverse": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "dependencies": { "@babel/highlight": "^7.10.4" } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/generator": { "version": "7.11.6", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", "dev": true, "dependencies": { "@babel/types": "^7.11.5", "jsesc": "^2.5.1", "source-map": "^0.5.0" } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/helper-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "dependencies": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "dependencies": { "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/helper-split-export-declaration": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "dependencies": { "@babel/types": "^7.11.0" } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/traverse": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.11.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", "@babel/parser": "^7.11.5", "@babel/types": "^7.11.5", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/helper-wrap-function/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-wrap-function/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-wrap-function/node_modules/debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "dependencies": { "ms": "^2.1.1" } }, "node_modules/@babel/helper-wrap-function/node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-wrap-function/node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "node_modules/@babel/helper-wrap-function/node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, "bin": { "jsesc": "bin/jsesc" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-wrap-function/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/helper-wrap-function/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "node_modules/@babel/helper-wrap-function/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { "has-flag": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/helper-wrap-function/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/helpers": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.4.tgz", "integrity": "sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w==", "dev": true, "dependencies": { "@babel/template": "^7.8.3", "@babel/traverse": "^7.8.4", "@babel/types": "^7.8.3" } }, "node_modules/@babel/highlight": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", "dev": true, "dependencies": { "chalk": "^2.0.0", "esutils": "^2.0.2", "js-tokens": "^4.0.0" } }, "node_modules/@babel/highlight/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/highlight/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/highlight/node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "node_modules/@babel/highlight/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { "has-flag": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/parser": { "version": "7.8.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz", "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==", "dev": true, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/plugin-proposal-async-generator-functions": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz", "integrity": "sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-remap-async-to-generator": "^7.10.4", "@babel/plugin-syntax-async-generators": "^7.8.0" } }, "node_modules/@babel/plugin-proposal-class-properties": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz", "integrity": "sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-proposal-dynamic-import": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz", "integrity": "sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-dynamic-import": "^7.8.0" } }, "node_modules/@babel/plugin-proposal-export-namespace-from": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz", "integrity": "sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" } }, "node_modules/@babel/plugin-proposal-json-strings": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz", "integrity": "sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.0" } }, "node_modules/@babel/plugin-proposal-logical-assignment-operators": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz", "integrity": "sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" } }, "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz", "integrity": "sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" } }, "node_modules/@babel/plugin-proposal-numeric-separator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz", "integrity": "sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-numeric-separator": "^7.10.4" } }, "node_modules/@babel/plugin-proposal-object-rest-spread": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz", "integrity": "sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", "@babel/plugin-transform-parameters": "^7.10.4" } }, "node_modules/@babel/plugin-proposal-optional-catch-binding": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz", "integrity": "sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" } }, "node_modules/@babel/plugin-proposal-optional-chaining": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz", "integrity": "sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0", "@babel/plugin-syntax-optional-chaining": "^7.8.0" } }, "node_modules/@babel/plugin-proposal-private-methods": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz", "integrity": "sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw==", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-proposal-unicode-property-regex": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz", "integrity": "sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" }, "engines": { "node": ">=4" } }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } }, "node_modules/@babel/plugin-syntax-class-properties": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } }, "node_modules/@babel/plugin-syntax-export-namespace-from": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" } }, "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } }, "node_modules/@babel/plugin-syntax-numeric-separator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-syntax-object-rest-spread": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } }, "node_modules/@babel/plugin-syntax-optional-catch-binding": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } }, "node_modules/@babel/plugin-syntax-optional-chaining": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" } }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz", "integrity": "sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz", "integrity": "sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-async-to-generator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz", "integrity": "sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==", "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-remap-async-to-generator": "^7.10.4" } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz", "integrity": "sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-block-scoping": { "version": "7.11.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz", "integrity": "sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-classes": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz", "integrity": "sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-define-map": "^7.10.4", "@babel/helper-function-name": "^7.10.4", "@babel/helper-optimise-call-expression": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-replace-supers": "^7.10.4", "@babel/helper-split-export-declaration": "^7.10.4", "globals": "^11.1.0" } }, "node_modules/@babel/plugin-transform-classes/node_modules/@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "dependencies": { "@babel/highlight": "^7.10.4" } }, "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "dependencies": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "dependencies": { "@babel/types": "^7.10.4" } }, "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "dependencies": { "@babel/types": "^7.11.0" } }, "node_modules/@babel/plugin-transform-classes/node_modules/@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "node_modules/@babel/plugin-transform-classes/node_modules/@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/plugin-transform-classes/node_modules/@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/plugin-transform-classes/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/plugin-transform-classes/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/plugin-transform-classes/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/plugin-transform-classes/node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/plugin-transform-classes/node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "node_modules/@babel/plugin-transform-classes/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/plugin-transform-classes/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { "has-flag": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/plugin-transform-classes/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz", "integrity": "sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-destructuring": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz", "integrity": "sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-dotall-regex": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz", "integrity": "sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-duplicate-keys": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz", "integrity": "sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz", "integrity": "sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==", "dev": true, "dependencies": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz", "integrity": "sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz", "integrity": "sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==", "dev": true, "dependencies": { "@babel/helper-function-name": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "dependencies": { "@babel/highlight": "^7.10.4" } }, "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/helper-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "dependencies": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "dependencies": { "@babel/types": "^7.10.4" } }, "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "node_modules/@babel/plugin-transform-function-name/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/plugin-transform-function-name/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/plugin-transform-function-name/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/plugin-transform-function-name/node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "node_modules/@babel/plugin-transform-function-name/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/plugin-transform-function-name/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { "has-flag": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/@babel/plugin-transform-function-name/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/plugin-transform-literals": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz", "integrity": "sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-member-expression-literals": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz", "integrity": "sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-modules-amd": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz", "integrity": "sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==", "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.10.5", "@babel/helper-plugin-utils": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz", "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==", "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-simple-access": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "node_modules/@babel/plugin-transform-modules-systemjs": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz", "integrity": "sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==", "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.10.4", "@babel/helper-module-transforms": "^7.10.5", "@babel/helper-plugin-utils": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz", "integrity": "sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==", "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz", "integrity": "sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.10.4" } }, "node_modules/@babel/plugin-transform-new-target": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz", "integrity": "sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-object-super": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz", "integrity": "sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-replace-supers": "^7.10.4" } }, "node_modules/@babel/plugin-transform-parameters": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz", "integrity": "sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==", "dev": true, "dependencies": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-parameters/node_modules/@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "dependencies": { "@babel/types": "^7.10.4" } }, "node_modules/@babel/plugin-transform-parameters/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/plugin-transform-parameters/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/plugin-transform-parameters/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/plugin-transform-property-literals": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz", "integrity": "sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz", "integrity": "sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==", "dev": true, "dependencies": { "regenerator-transform": "^0.14.2" } }, "node_modules/@babel/plugin-transform-regenerator/node_modules/regenerator-transform": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", "dev": true, "dependencies": { "@babel/runtime": "^7.8.4" } }, "node_modules/@babel/plugin-transform-reserved-words": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz", "integrity": "sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz", "integrity": "sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-spread": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz", "integrity": "sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0" } }, "node_modules/@babel/plugin-transform-sticky-regex": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz", "integrity": "sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-regex": "^7.10.4" } }, "node_modules/@babel/plugin-transform-template-literals": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz", "integrity": "sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-typeof-symbol": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz", "integrity": "sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz", "integrity": "sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz", "integrity": "sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "node_modules/@babel/preset-env": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.5.tgz", "integrity": "sha512-kXqmW1jVcnB2cdueV+fyBM8estd5mlNfaQi6lwLgRwCby4edpavgbFhiBNjmWA3JpB/yZGSISa7Srf+TwxDQoA==", "dev": true, "dependencies": { "@babel/compat-data": "^7.11.0", "@babel/helper-compilation-targets": "^7.10.4", "@babel/helper-module-imports": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-proposal-async-generator-functions": "^7.10.4", "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-dynamic-import": "^7.10.4", "@babel/plugin-proposal-export-namespace-from": "^7.10.4", "@babel/plugin-proposal-json-strings": "^7.10.4", "@babel/plugin-proposal-logical-assignment-operators": "^7.11.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", "@babel/plugin-proposal-numeric-separator": "^7.10.4", "@babel/plugin-proposal-object-rest-spread": "^7.11.0", "@babel/plugin-proposal-optional-catch-binding": "^7.10.4", "@babel/plugin-proposal-optional-chaining": "^7.11.0", "@babel/plugin-proposal-private-methods": "^7.10.4", "@babel/plugin-proposal-unicode-property-regex": "^7.10.4", "@babel/plugin-syntax-async-generators": "^7.8.0", "@babel/plugin-syntax-class-properties": "^7.10.4", "@babel/plugin-syntax-dynamic-import": "^7.8.0", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", "@babel/plugin-syntax-json-strings": "^7.8.0", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", "@babel/plugin-syntax-optional-chaining": "^7.8.0", "@babel/plugin-syntax-top-level-await": "^7.10.4", "@babel/plugin-transform-arrow-functions": "^7.10.4", "@babel/plugin-transform-async-to-generator": "^7.10.4", "@babel/plugin-transform-block-scoped-functions": "^7.10.4", "@babel/plugin-transform-block-scoping": "^7.10.4", "@babel/plugin-transform-classes": "^7.10.4", "@babel/plugin-transform-computed-properties": "^7.10.4", "@babel/plugin-transform-destructuring": "^7.10.4", "@babel/plugin-transform-dotall-regex": "^7.10.4", "@babel/plugin-transform-duplicate-keys": "^7.10.4", "@babel/plugin-transform-exponentiation-operator": "^7.10.4", "@babel/plugin-transform-for-of": "^7.10.4", "@babel/plugin-transform-function-name": "^7.10.4", "@babel/plugin-transform-literals": "^7.10.4", "@babel/plugin-transform-member-expression-literals": "^7.10.4", "@babel/plugin-transform-modules-amd": "^7.10.4", "@babel/plugin-transform-modules-commonjs": "^7.10.4", "@babel/plugin-transform-modules-systemjs": "^7.10.4", "@babel/plugin-transform-modules-umd": "^7.10.4", "@babel/plugin-transform-named-capturing-groups-regex": "^7.10.4", "@babel/plugin-transform-new-target": "^7.10.4", "@babel/plugin-transform-object-super": "^7.10.4", "@babel/plugin-transform-parameters": "^7.10.4", "@babel/plugin-transform-property-literals": "^7.10.4", "@babel/plugin-transform-regenerator": "^7.10.4", "@babel/plugin-transform-reserved-words": "^7.10.4", "@babel/plugin-transform-shorthand-properties": "^7.10.4", "@babel/plugin-transform-spread": "^7.11.0", "@babel/plugin-transform-sticky-regex": "^7.10.4", "@babel/plugin-transform-template-literals": "^7.10.4", "@babel/plugin-transform-typeof-symbol": "^7.10.4", "@babel/plugin-transform-unicode-escapes": "^7.10.4", "@babel/plugin-transform-unicode-regex": "^7.10.4", "@babel/preset-modules": "^0.1.3", "@babel/types": "^7.11.5", "browserslist": "^4.12.0", "core-js-compat": "^3.6.2", "invariant": "^2.2.2", "levenary": "^1.1.1", "semver": "^5.5.0" } }, "node_modules/@babel/preset-env/node_modules/@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/preset-env/node_modules/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "node_modules/@babel/preset-env/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/preset-modules": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", "@babel/plugin-transform-dotall-regex": "^7.4.4", "@babel/types": "^7.4.4", "esutils": "^2.0.2" } }, "node_modules/@babel/runtime": { "version": "7.11.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.13.4" } }, "node_modules/@babel/runtime/node_modules/regenerator-runtime": { "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", "dev": true }, "node_modules/@babel/template": { "version": "7.8.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", "dev": true, "dependencies": { "@babel/code-frame": "^7.8.3", "@babel/parser": "^7.8.6", "@babel/types": "^7.8.6" } }, "node_modules/@babel/traverse": { "version": "7.8.6", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", "dev": true, "dependencies": { "@babel/code-frame": "^7.8.3", "@babel/generator": "^7.8.6", "@babel/helper-function-name": "^7.8.3", "@babel/helper-split-export-declaration": "^7.8.3", "@babel/parser": "^7.8.6", "@babel/types": "^7.8.6", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.13" } }, "node_modules/@babel/traverse/node_modules/debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "dependencies": { "ms": "^2.1.1" } }, "node_modules/@babel/traverse/node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/traverse/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "node_modules/@babel/types": { "version": "7.8.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", "dev": true, "dependencies": { "esutils": "^2.0.2", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } }, "node_modules/@babel/types/node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", "dependencies": { "@webassemblyjs/helper-module-context": "1.8.5", "@webassemblyjs/helper-wasm-bytecode": "1.8.5", "@webassemblyjs/wast-parser": "1.8.5" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==" }, "node_modules/@webassemblyjs/helper-code-frame": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", "dependencies": { "@webassemblyjs/wast-printer": "1.8.5" } }, "node_modules/@webassemblyjs/helper-fsm": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==" }, "node_modules/@webassemblyjs/helper-module-context": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", "dependencies": { "@webassemblyjs/ast": "1.8.5", "mamacro": "^0.0.3" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", "dependencies": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-buffer": "1.8.5", "@webassemblyjs/helper-wasm-bytecode": "1.8.5", "@webassemblyjs/wasm-gen": "1.8.5" } }, "node_modules/@webassemblyjs/ieee754": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", "dependencies": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-buffer": "1.8.5", "@webassemblyjs/helper-wasm-bytecode": "1.8.5", "@webassemblyjs/helper-wasm-section": "1.8.5", "@webassemblyjs/wasm-gen": "1.8.5", "@webassemblyjs/wasm-opt": "1.8.5", "@webassemblyjs/wasm-parser": "1.8.5", "@webassemblyjs/wast-printer": "1.8.5" } }, "node_modules/@webassemblyjs/wasm-gen": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", "dependencies": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-wasm-bytecode": "1.8.5", "@webassemblyjs/ieee754": "1.8.5", "@webassemblyjs/leb128": "1.8.5", "@webassemblyjs/utf8": "1.8.5" } }, "node_modules/@webassemblyjs/wasm-opt": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", "dependencies": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-buffer": "1.8.5", "@webassemblyjs/wasm-gen": "1.8.5", "@webassemblyjs/wasm-parser": "1.8.5" } }, "node_modules/@webassemblyjs/wasm-parser": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", "dependencies": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-api-error": "1.8.5", "@webassemblyjs/helper-wasm-bytecode": "1.8.5", "@webassemblyjs/ieee754": "1.8.5", "@webassemblyjs/leb128": "1.8.5", "@webassemblyjs/utf8": "1.8.5" } }, "node_modules/@webassemblyjs/wast-parser": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", "dependencies": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/floating-point-hex-parser": "1.8.5", "@webassemblyjs/helper-api-error": "1.8.5", "@webassemblyjs/helper-code-frame": "1.8.5", "@webassemblyjs/helper-fsm": "1.8.5", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/wast-printer": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", "dependencies": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/wast-parser": "1.8.5", "@xtuc/long": "4.2.2" } }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, "node_modules/acorn": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", "bin": { "acorn": "bin/acorn" }, "engines": { "node": ">=0.4.0" } }, "node_modules/ajv": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "node_modules/ajv-errors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==" }, "node_modules/ajv-keywords": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==" }, "node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "engines": { "node": ">=0.10.0" } }, "node_modules/ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "engines": { "node": ">=0.10.0" } }, "node_modules/anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "dependencies": { "micromatch": "^3.1.4", "normalize-path": "^2.1.1" } }, "node_modules/anymatch/node_modules/normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dependencies": { "remove-trailing-separator": "^1.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", "engines": { "node": ">=0.10.0" } }, "node_modules/arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", "engines": { "node": ">=0.10.0" } }, "node_modules/arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "engines": { "node": ">=0.10.0" } }, "node_modules/array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "engines": { "node": ">=0.10.0" } }, "node_modules/asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "node_modules/assert": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", "dependencies": { "object-assign": "^4.1.1", "util": "0.10.3" } }, "node_modules/assert/node_modules/inherits": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" }, "node_modules/assert/node_modules/util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dependencies": { "inherits": "2.0.1" } }, "node_modules/assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "engines": { "node": ">=0.10.0" } }, "node_modules/async-each": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" }, "node_modules/atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "bin": { "atob": "bin/atob.js" }, "engines": { "node": ">= 4.5.0" } }, "node_modules/babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", "dependencies": { "chalk": "^1.1.3", "esutils": "^2.0.2", "js-tokens": "^3.0.2" } }, "node_modules/babel-core": { "version": "6.26.3", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", "dependencies": { "babel-code-frame": "^6.26.0", "babel-generator": "^6.26.0", "babel-helpers": "^6.24.1", "babel-messages": "^6.23.0", "babel-register": "^6.26.0", "babel-runtime": "^6.26.0", "babel-template": "^6.26.0", "babel-traverse": "^6.26.0", "babel-types": "^6.26.0", "babylon": "^6.18.0", "convert-source-map": "^1.5.1", "debug": "^2.6.9", "json5": "^0.5.1", "lodash": "^4.17.4", "minimatch": "^3.0.4", "path-is-absolute": "^1.0.1", "private": "^0.1.8", "slash": "^1.0.0", "source-map": "^0.5.7" } }, "node_modules/babel-core/node_modules/json5": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "bin": { "json5": "lib/cli.js" } }, "node_modules/babel-generator": { "version": "6.26.1", "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", "dependencies": { "babel-messages": "^6.23.0", "babel-runtime": "^6.26.0", "babel-types": "^6.26.0", "detect-indent": "^4.0.0", "jsesc": "^1.3.0", "lodash": "^4.17.4", "source-map": "^0.5.7", "trim-right": "^1.0.1" } }, "node_modules/babel-helper-call-delegate": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", "dependencies": { "babel-helper-hoist-variables": "^6.24.1", "babel-runtime": "^6.22.0", "babel-traverse": "^6.24.1", "babel-types": "^6.24.1" } }, "node_modules/babel-helper-define-map": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", "dependencies": { "babel-helper-function-name": "^6.24.1", "babel-runtime": "^6.26.0", "babel-types": "^6.26.0", "lodash": "^4.17.4" } }, "node_modules/babel-helper-function-name": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", "dependencies": { "babel-helper-get-function-arity": "^6.24.1", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1", "babel-traverse": "^6.24.1", "babel-types": "^6.24.1" } }, "node_modules/babel-helper-get-function-arity": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", "dependencies": { "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "node_modules/babel-helper-hoist-variables": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", "dependencies": { "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "node_modules/babel-helper-optimise-call-expression": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", "dependencies": { "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "node_modules/babel-helper-regex": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", "dependencies": { "babel-runtime": "^6.26.0", "babel-types": "^6.26.0", "lodash": "^4.17.4" } }, "node_modules/babel-helper-replace-supers": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", "dependencies": { "babel-helper-optimise-call-expression": "^6.24.1", "babel-messages": "^6.23.0", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1", "babel-traverse": "^6.24.1", "babel-types": "^6.24.1" } }, "node_modules/babel-helpers": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", "dependencies": { "babel-runtime": "^6.22.0", "babel-template": "^6.24.1" } }, "node_modules/babel-loader": { "version": "8.0.6", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz", "integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==", "dev": true, "dependencies": { "find-cache-dir": "^2.0.0", "loader-utils": "^1.0.2", "mkdirp": "^0.5.1", "pify": "^4.0.1" }, "engines": { "node": ">= 6.9" } }, "node_modules/babel-messages": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", "dependencies": { "babel-runtime": "^6.22.0" } }, "node_modules/babel-plugin-check-es2015-constants": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", "dependencies": { "babel-runtime": "^6.22.0" } }, "node_modules/babel-plugin-dynamic-import-node": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", "dev": true, "dependencies": { "object.assign": "^4.1.0" } }, "node_modules/babel-plugin-transform-es2015-arrow-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", "dependencies": { "babel-runtime": "^6.22.0" } }, "node_modules/babel-plugin-transform-es2015-block-scoped-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", "dependencies": { "babel-runtime": "^6.22.0" } }, "node_modules/babel-plugin-transform-es2015-block-scoping": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", "dependencies": { "babel-runtime": "^6.26.0", "babel-template": "^6.26.0", "babel-traverse": "^6.26.0", "babel-types": "^6.26.0", "lodash": "^4.17.4" } }, "node_modules/babel-plugin-transform-es2015-classes": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", "dependencies": { "babel-helper-define-map": "^6.24.1", "babel-helper-function-name": "^6.24.1", "babel-helper-optimise-call-expression": "^6.24.1", "babel-helper-replace-supers": "^6.24.1", "babel-messages": "^6.23.0", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1", "babel-traverse": "^6.24.1", "babel-types": "^6.24.1" } }, "node_modules/babel-plugin-transform-es2015-computed-properties": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", "dependencies": { "babel-runtime": "^6.22.0", "babel-template": "^6.24.1" } }, "node_modules/babel-plugin-transform-es2015-destructuring": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", "dependencies": { "babel-runtime": "^6.22.0" } }, "node_modules/babel-plugin-transform-es2015-duplicate-keys": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", "dependencies": { "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "node_modules/babel-plugin-transform-es2015-for-of": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", "dependencies": { "babel-runtime": "^6.22.0" } }, "node_modules/babel-plugin-transform-es2015-function-name": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", "dependencies": { "babel-helper-function-name": "^6.24.1", "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "node_modules/babel-plugin-transform-es2015-literals": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", "dependencies": { "babel-runtime": "^6.22.0" } }, "node_modules/babel-plugin-transform-es2015-modules-amd": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", "dependencies": { "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1" } }, "node_modules/babel-plugin-transform-es2015-modules-commonjs": { "version": "6.26.2", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", "dependencies": { "babel-plugin-transform-strict-mode": "^6.24.1", "babel-runtime": "^6.26.0", "babel-template": "^6.26.0", "babel-types": "^6.26.0" } }, "node_modules/babel-plugin-transform-es2015-modules-systemjs": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", "dependencies": { "babel-helper-hoist-variables": "^6.24.1", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1" } }, "node_modules/babel-plugin-transform-es2015-modules-umd": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", "dependencies": { "babel-plugin-transform-es2015-modules-amd": "^6.24.1", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1" } }, "node_modules/babel-plugin-transform-es2015-object-super": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", "dependencies": { "babel-helper-replace-supers": "^6.24.1", "babel-runtime": "^6.22.0" } }, "node_modules/babel-plugin-transform-es2015-parameters": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", "dependencies": { "babel-helper-call-delegate": "^6.24.1", "babel-helper-get-function-arity": "^6.24.1", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1", "babel-traverse": "^6.24.1", "babel-types": "^6.24.1" } }, "node_modules/babel-plugin-transform-es2015-shorthand-properties": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", "dependencies": { "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "node_modules/babel-plugin-transform-es2015-spread": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", "dependencies": { "babel-runtime": "^6.22.0" } }, "node_modules/babel-plugin-transform-es2015-sticky-regex": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", "dependencies": { "babel-helper-regex": "^6.24.1", "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "node_modules/babel-plugin-transform-es2015-template-literals": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", "dependencies": { "babel-runtime": "^6.22.0" } }, "node_modules/babel-plugin-transform-es2015-typeof-symbol": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", "dependencies": { "babel-runtime": "^6.22.0" } }, "node_modules/babel-plugin-transform-es2015-unicode-regex": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", "dependencies": { "babel-helper-regex": "^6.24.1", "babel-runtime": "^6.22.0", "regexpu-core": "^2.0.0" } }, "node_modules/babel-plugin-transform-regenerator": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", "dependencies": { "regenerator-transform": "^0.10.0" } }, "node_modules/babel-plugin-transform-strict-mode": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", "dependencies": { "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "node_modules/babel-preset-es2015": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", "dependencies": { "babel-plugin-check-es2015-constants": "^6.22.0", "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", "babel-plugin-transform-es2015-block-scoping": "^6.24.1", "babel-plugin-transform-es2015-classes": "^6.24.1", "babel-plugin-transform-es2015-computed-properties": "^6.24.1", "babel-plugin-transform-es2015-destructuring": "^6.22.0", "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1", "babel-plugin-transform-es2015-for-of": "^6.22.0", "babel-plugin-transform-es2015-function-name": "^6.24.1", "babel-plugin-transform-es2015-literals": "^6.22.0", "babel-plugin-transform-es2015-modules-amd": "^6.24.1", "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", "babel-plugin-transform-es2015-modules-umd": "^6.24.1", "babel-plugin-transform-es2015-object-super": "^6.24.1", "babel-plugin-transform-es2015-parameters": "^6.24.1", "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1", "babel-plugin-transform-es2015-spread": "^6.22.0", "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", "babel-plugin-transform-es2015-template-literals": "^6.22.0", "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0", "babel-plugin-transform-es2015-unicode-regex": "^6.24.1", "babel-plugin-transform-regenerator": "^6.24.1" } }, "node_modules/babel-register": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", "dependencies": { "babel-core": "^6.26.0", "babel-runtime": "^6.26.0", "core-js": "^2.5.0", "home-or-tmp": "^2.0.0", "lodash": "^4.17.4", "mkdirp": "^0.5.1", "source-map-support": "^0.4.15" } }, "node_modules/babel-register/node_modules/source-map-support": { "version": "0.4.18", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", "dependencies": { "source-map": "^0.5.6" } }, "node_modules/babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dependencies": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" } }, "node_modules/babel-template": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", "dependencies": { "babel-runtime": "^6.26.0", "babel-traverse": "^6.26.0", "babel-types": "^6.26.0", "babylon": "^6.18.0", "lodash": "^4.17.4" } }, "node_modules/babel-traverse": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", "dependencies": { "babel-code-frame": "^6.26.0", "babel-messages": "^6.23.0", "babel-runtime": "^6.26.0", "babel-types": "^6.26.0", "babylon": "^6.18.0", "debug": "^2.6.8", "globals": "^9.18.0", "invariant": "^2.2.2", "lodash": "^4.17.4" } }, "node_modules/babel-types": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "dependencies": { "babel-runtime": "^6.26.0", "esutils": "^2.0.2", "lodash": "^4.17.4", "to-fast-properties": "^1.0.3" } }, "node_modules/babylon": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", "bin": { "babylon": "bin/babylon.js" } }, "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "node_modules/base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", "dependencies": { "cache-base": "^1.0.1", "class-utils": "^0.3.5", "component-emitter": "^1.2.1", "define-property": "^1.0.0", "isobject": "^3.0.1", "mixin-deep": "^1.2.0", "pascalcase": "^0.1.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/base/node_modules/define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dependencies": { "is-descriptor": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/base/node_modules/is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dependencies": { "kind-of": "^6.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/base/node_modules/is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dependencies": { "kind-of": "^6.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/base/node_modules/is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dependencies": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" }, "engines": { "node": ">=0.10.0" } }, "node_modules/base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "engines": { "node": "*" } }, "node_modules/binary-extensions": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "engines": { "node": ">=0.10.0" } }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "optional": true, "dependencies": { "file-uri-to-path": "1.0.0" } }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "node_modules/bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/braces": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dependencies": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", "extend-shallow": "^2.0.1", "fill-range": "^4.0.0", "isobject": "^3.0.1", "repeat-element": "^1.1.2", "snapdragon": "^0.8.1", "snapdragon-node": "^2.0.1", "split-string": "^3.0.2", "to-regex": "^3.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/braces/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dependencies": { "is-extendable": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "node_modules/browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.3", "inherits": "^2.0.1", "safe-buffer": "^5.0.1" } }, "node_modules/browserify-cipher": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", "dependencies": { "browserify-aes": "^1.0.4", "browserify-des": "^1.0.0", "evp_bytestokey": "^1.0.0" } }, "node_modules/browserify-des": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", "dependencies": { "cipher-base": "^1.0.1", "des.js": "^1.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "node_modules/browserify-rsa": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dependencies": { "bn.js": "^4.1.0", "randombytes": "^2.0.1" } }, "node_modules/browserify-sign": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", "dependencies": { "bn.js": "^4.1.1", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "create-hmac": "^1.1.2", "elliptic": "^6.0.0", "inherits": "^2.0.1", "parse-asn1": "^5.0.0" } }, "node_modules/browserify-zlib": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", "dependencies": { "pako": "~1.0.5" } }, "node_modules/browserslist": { "version": "4.14.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.1.tgz", "integrity": "sha512-zyBTIHydW37pnb63c7fHFXUG6EcqWOqoMdDx6cdyaDFriZ20EoVxcE95S54N+heRqY8m8IUgB5zYta/gCwSaaA==", "dev": true, "dependencies": { "caniuse-lite": "^1.0.30001124", "electron-to-chromium": "^1.3.562", "escalade": "^3.0.2", "node-releases": "^1.1.60" }, "bin": { "browserslist": "cli.js" }, "engines": { "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, "node_modules/buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "node_modules/buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "node_modules/buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, "node_modules/builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" }, "node_modules/cacache": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", "dependencies": { "bluebird": "^3.5.5", "chownr": "^1.1.1", "figgy-pudding": "^3.5.1", "glob": "^7.1.4", "graceful-fs": "^4.1.15", "infer-owner": "^1.0.3", "lru-cache": "^5.1.1", "mississippi": "^3.0.0", "mkdirp": "^0.5.1", "move-concurrently": "^1.0.1", "promise-inflight": "^1.0.1", "rimraf": "^2.6.3", "ssri": "^6.0.1", "unique-filename": "^1.1.1", "y18n": "^4.0.0" } }, "node_modules/cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", "dependencies": { "collection-visit": "^1.0.0", "component-emitter": "^1.2.1", "get-value": "^2.0.6", "has-value": "^1.0.0", "isobject": "^3.0.1", "set-value": "^2.0.0", "to-object-path": "^0.3.0", "union-value": "^1.0.0", "unset-value": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "engines": { "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001125", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001125.tgz", - "integrity": "sha512-9f+r7BW8Qli917mU3j0fUaTweT3f3vnX/Lcs+1C73V+RADmFme+Ih0Br8vONQi3X0lseOe6ZHfsZLCA8MSjxUA==", - "dev": true + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "dependencies": { "anymatch": "^2.0.0", "async-each": "^1.0.1", "braces": "^2.3.2", "glob-parent": "^3.1.0", "inherits": "^2.0.3", "is-binary-path": "^1.0.0", "is-glob": "^4.0.0", "normalize-path": "^3.0.0", "path-is-absolute": "^1.0.0", "readdirp": "^2.2.1", "upath": "^1.1.1" }, "optionalDependencies": { "fsevents": "^1.2.7" } }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chrome-trace-event": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", "dependencies": { "tslib": "^1.9.0" }, "engines": { "node": ">=6.0" } }, "node_modules/cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" } }, "node_modules/class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", "dependencies": { "arr-union": "^3.1.0", "define-property": "^0.2.5", "isobject": "^3.0.0", "static-extend": "^0.1.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/class-utils/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dependencies": { "is-descriptor": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "dependencies": { "string-width": "^3.1.0", "strip-ansi": "^5.2.0", "wrap-ansi": "^5.1.0" } }, "node_modules/cliui/node_modules/ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "engines": { "node": ">=6" } }, "node_modules/cliui/node_modules/strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dependencies": { "ansi-regex": "^4.1.0" }, "engines": { "node": ">=6" } }, "node_modules/collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", "dependencies": { "map-visit": "^1.0.0", "object-visit": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dependencies": { "color-name": "1.1.3" } }, "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "node_modules/concat-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "engines": [ "node >= 0.8" ], "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "node_modules/console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" }, "node_modules/constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" }, "node_modules/convert-source-map": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", "dependencies": { "safe-buffer": "~5.1.1" } }, "node_modules/copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", "dependencies": { "aproba": "^1.1.1", "fs-write-stream-atomic": "^1.0.8", "iferr": "^0.1.5", "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.0" } }, "node_modules/copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "engines": { "node": ">=0.10.0" } }, "node_modules/core-js": { "version": "2.6.11", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", "hasInstallScript": true }, "node_modules/core-js-compat": { "version": "3.6.5", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz", "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==", "dev": true, "dependencies": { "browserslist": "^4.8.5", "semver": "7.0.0" } }, "node_modules/core-js-compat/node_modules/semver": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "node_modules/create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", "dependencies": { "bn.js": "^4.1.0", "elliptic": "^6.0.0" } }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", "md5.js": "^1.3.4", "ripemd160": "^2.0.1", "sha.js": "^2.4.0" } }, "node_modules/create-hmac": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", "inherits": "^2.0.1", "ripemd160": "^2.0.0", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "node_modules/crel": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/crel/-/crel-4.2.0.tgz", "integrity": "sha512-ZBVhTmtapdjEn8rTcTR/ePE0JSNfHzZfDkD3eAnsQEBmkQW9r14mzq6tK5Hkw6r26l7XxlU2Ox1nfHsmGIj0zQ==" }, "node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" }, "engines": { "node": ">=4.8" } }, "node_modules/crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", "dependencies": { "browserify-cipher": "^1.0.0", "browserify-sign": "^4.0.0", "create-ecdh": "^4.0.0", "create-hash": "^1.1.0", "create-hmac": "^1.1.0", "diffie-hellman": "^5.0.0", "inherits": "^2.0.1", "pbkdf2": "^3.0.3", "public-encrypt": "^4.0.0", "randombytes": "^2.0.0", "randomfill": "^1.0.3" }, "engines": { "node": "*" } }, "node_modules/cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=" }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dependencies": { "ms": "2.0.0" } }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "engines": { "node": ">=0.10.0" } }, "node_modules/decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "engines": { "node": ">=0.10" } }, "node_modules/define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "dependencies": { "object-keys": "^1.0.12" }, "engines": { "node": ">= 0.4" } }, "node_modules/define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", "dependencies": { "is-descriptor": "^1.0.2", "isobject": "^3.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/define-property/node_modules/is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dependencies": { "kind-of": "^6.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/define-property/node_modules/is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dependencies": { "kind-of": "^6.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/define-property/node_modules/is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dependencies": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" }, "engines": { "node": ">=0.10.0" } }, "node_modules/des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "node_modules/detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", "engines": { "node": ">=0.10.0" } }, "node_modules/detect-indent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", "dependencies": { "repeating": "^2.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dependencies": { "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" } }, "node_modules/domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "engines": { "node": ">=0.4", "npm": ">=1.2" } }, "node_modules/duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "node_modules/electron-to-chromium": { "version": "1.3.564", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.564.tgz", "integrity": "sha512-fNaYN3EtKQWLQsrKXui8mzcryJXuA0LbCLoizeX6oayG2emBaS5MauKjCPAvc29NEY4FpLHIUWiP+Y0Bfrs5dg==", "dev": true }, "node_modules/elliptic": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", "dependencies": { "bn.js": "^4.4.0", "brorand": "^1.0.1", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.0" } }, "node_modules/emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "engines": { "node": ">= 4" } }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dependencies": { "once": "^1.4.0" } }, "node_modules/enhanced-resolve": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", "dependencies": { "graceful-fs": "^4.1.2", "memory-fs": "^0.5.0", "tapable": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/enhanced-resolve/node_modules/memory-fs": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", "dependencies": { "errno": "^0.1.3", "readable-stream": "^2.0.1" }, "engines": { "node": ">=4.3.0 <5.0.0 || >=5.10" } }, "node_modules/entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" }, "node_modules/errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", "dependencies": { "prr": "~1.0.1" }, "bin": { "errno": "cli.js" } }, "node_modules/escalade": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz", "integrity": "sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "engines": { "node": ">=0.8.0" } }, "node_modules/eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", "dependencies": { "esrecurse": "^4.1.0", "estraverse": "^4.1.1" }, "engines": { "node": ">=4.0.0" } }, "node_modules/esrecurse": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", "dependencies": { "estraverse": "^4.1.0" }, "engines": { "node": ">=4.0" } }, "node_modules/estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "engines": { "node": ">=4.0" } }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "engines": { "node": ">=0.10.0" } }, "node_modules/events": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", "engines": { "node": ">=0.8.x" } }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" } }, "node_modules/execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" }, "engines": { "node": ">=6" } }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", "dependencies": { "debug": "^2.3.3", "define-property": "^0.2.5", "extend-shallow": "^2.0.1", "posix-character-classes": "^0.1.0", "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/expand-brackets/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dependencies": { "is-descriptor": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/expand-brackets/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dependencies": { "is-extendable": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", "dependencies": { "homedir-polyfill": "^1.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/extend-shallow/node_modules/is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dependencies": { "is-plain-object": "^2.0.4" }, "engines": { "node": ">=0.10.0" } }, "node_modules/extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", "dependencies": { "array-unique": "^0.3.2", "define-property": "^1.0.0", "expand-brackets": "^2.1.4", "extend-shallow": "^2.0.1", "fragment-cache": "^0.2.1", "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/extglob/node_modules/define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dependencies": { "is-descriptor": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/extglob/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dependencies": { "is-extendable": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/extglob/node_modules/is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dependencies": { "kind-of": "^6.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/extglob/node_modules/is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dependencies": { "kind-of": "^6.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/extglob/node_modules/is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dependencies": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" }, "engines": { "node": ">=0.10.0" } }, "node_modules/fast-deep-equal": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/figgy-pudding": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==" }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "optional": true }, "node_modules/fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dependencies": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", "repeat-string": "^1.6.1", "to-regex-range": "^2.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/fill-range/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dependencies": { "is-extendable": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", "dependencies": { "commondir": "^1.0.1", "make-dir": "^2.0.0", "pkg-dir": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dependencies": { "locate-path": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/findup-sync": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", "dependencies": { "detect-file": "^1.0.0", "is-glob": "^4.0.0", "micromatch": "^3.0.4", "resolve-dir": "^1.0.1" }, "engines": { "node": ">= 0.10" } }, "node_modules/flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", "dependencies": { "inherits": "^2.0.3", "readable-stream": "^2.3.6" } }, "node_modules/for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "engines": { "node": ">=0.10.0" } }, "node_modules/fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", "dependencies": { "map-cache": "^0.2.2" }, "engines": { "node": ">=0.10.0" } }, "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", "dependencies": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" } }, "node_modules/fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", "dependencies": { "graceful-fs": "^4.1.2", "iferr": "^0.1.5", "imurmurhash": "^0.1.4", "readable-stream": "1 || 2" } }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "node_modules/fsevents": { "version": "1.2.11", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.11.tgz", "integrity": "sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==", "bundleDependencies": [ "abbrev", "ansi-regex", "aproba", "are-we-there-yet", "balanced-match", "brace-expansion", "chownr", "code-point-at", "concat-map", "console-control-strings", "core-util-is", "debug", "deep-extend", "delegates", "detect-libc", "fs-minipass", "fs.realpath", "gauge", "glob", "has-unicode", "iconv-lite", "ignore-walk", "inflight", "inherits", "ini", "is-fullwidth-code-point", "isarray", "minimatch", "minimist", "minipass", "minizlib", "mkdirp", "ms", "needle", "node-pre-gyp", "nopt", "npm-bundled", "npm-normalize-package-bin", "npm-packlist", "npmlog", "number-is-nan", "object-assign", "once", "os-homedir", "os-tmpdir", "osenv", "path-is-absolute", "process-nextick-args", "rc", "readable-stream", "rimraf", "safe-buffer", "safer-buffer", "sax", "semver", "set-blocking", "signal-exit", "string-width", "string_decoder", "strip-ansi", "strip-json-comments", "tar", "util-deprecate", "wide-align", "wrappy", "yallist" ], "hasInstallScript": true, "optional": true, "os": [ "darwin" ], "dependencies": { "bindings": "^1.5.0", "nan": "^2.12.1", "node-pre-gyp": "*" }, "engines": { "node": ">=4.0" } }, "node_modules/fsevents/node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "inBundle": true, "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "inBundle": true, "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "inBundle": true, "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/are-we-there-yet": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" } }, "node_modules/fsevents/node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "inBundle": true, "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "inBundle": true, "license": "MIT", "optional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/fsevents/node_modules/chownr": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", "inBundle": true, "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "inBundle": true, "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "inBundle": true, "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "inBundle": true, "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "inBundle": true, "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "inBundle": true, "license": "MIT", "optional": true, "dependencies": { "ms": "^2.1.1" } }, "node_modules/fsevents/node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "inBundle": true, "license": "MIT", "optional": true, "engines": { "node": ">=4.0.0" } }, "node_modules/fsevents/node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "inBundle": true, "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "inBundle": true, "license": "Apache-2.0", "optional": true, "bin": { "detect-libc": "bin/detect-libc.js" }, "engines": { "node": ">=0.10" } }, "node_modules/fsevents/node_modules/fs-minipass": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "minipass": "^2.6.0" } }, "node_modules/fsevents/node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "inBundle": true, "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", "has-unicode": "^2.0.0", "object-assign": "^4.1.0", "signal-exit": "^3.0.0", "string-width": "^1.0.1", "strip-ansi": "^3.0.1", "wide-align": "^1.1.0" } }, "node_modules/fsevents/node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, "engines": { "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/fsevents/node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "inBundle": true, "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "inBundle": true, "license": "MIT", "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/ignore-walk": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "minimatch": "^3.0.4" } }, "node_modules/fsevents/node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "node_modules/fsevents/node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "inBundle": true, "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "inBundle": true, "license": "ISC", "optional": true, "engines": { "node": "*" } }, "node_modules/fsevents/node_modules/is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "inBundle": true, "license": "MIT", "optional": true, "dependencies": { "number-is-nan": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "inBundle": true, "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "brace-expansion": "^1.1.7" }, "engines": { "node": "*" } }, "node_modules/fsevents/node_modules/minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "inBundle": true, "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/minipass": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "node_modules/fsevents/node_modules/minizlib": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", "inBundle": true, "license": "MIT", "optional": true, "dependencies": { "minipass": "^2.9.0" } }, "node_modules/fsevents/node_modules/mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "inBundle": true, "license": "MIT", "optional": true, "dependencies": { "minimist": "0.0.8" }, "bin": { "mkdirp": "bin/cmd.js" } }, "node_modules/fsevents/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "inBundle": true, "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/needle": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", "inBundle": true, "license": "MIT", "optional": true, "dependencies": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", "sax": "^1.2.4" }, "bin": { "needle": "bin/needle" }, "engines": { "node": ">= 4.4.x" } }, "node_modules/fsevents/node_modules/node-pre-gyp": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz", "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==", "inBundle": true, "license": "BSD-3-Clause", "optional": true, "dependencies": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", "needle": "^2.2.1", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4.4.2" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "node_modules/fsevents/node_modules/nopt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "abbrev": "1", "osenv": "^0.1.4" }, "bin": { "nopt": "bin/nopt.js" } }, "node_modules/fsevents/node_modules/npm-bundled": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "npm-normalize-package-bin": "^1.0.1" } }, "node_modules/fsevents/node_modules/npm-normalize-package-bin": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", "inBundle": true, "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/npm-packlist": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.7.tgz", "integrity": "sha512-vAj7dIkp5NhieaGZxBJB8fF4R0078rqsmhJcAfXZ6O7JJhjhPK96n5Ry1oZcfLXgfun0GWTZPOxaEyqv8GBykQ==", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1" } }, "node_modules/fsevents/node_modules/npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", "gauge": "~2.7.3", "set-blocking": "~2.0.0" } }, "node_modules/fsevents/node_modules/number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "inBundle": true, "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "inBundle": true, "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "wrappy": "1" } }, "node_modules/fsevents/node_modules/os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "inBundle": true, "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "inBundle": true, "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/osenv": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" } }, "node_modules/fsevents/node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "inBundle": true, "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "inBundle": true, "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "inBundle": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "optional": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "cli.js" } }, "node_modules/fsevents/node_modules/rc/node_modules/minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "inBundle": true, "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "inBundle": true, "license": "MIT", "optional": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "node_modules/fsevents/node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "node_modules/fsevents/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "inBundle": true, "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "inBundle": true, "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "inBundle": true, "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "inBundle": true, "license": "ISC", "optional": true, "bin": { "semver": "bin/semver" } }, "node_modules/fsevents/node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "inBundle": true, "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "inBundle": true, "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "inBundle": true, "license": "MIT", "optional": true, "dependencies": { "safe-buffer": "~5.1.0" } }, "node_modules/fsevents/node_modules/string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "inBundle": true, "license": "MIT", "optional": true, "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "inBundle": true, "license": "MIT", "optional": true, "dependencies": { "ansi-regex": "^2.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "inBundle": true, "license": "MIT", "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/tar": { "version": "4.4.13", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", "minipass": "^2.8.6", "minizlib": "^1.2.1", "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", "yallist": "^3.0.3" }, "engines": { "node": ">=4.5" } }, "node_modules/fsevents/node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "inBundle": true, "license": "MIT", "optional": true }, "node_modules/fsevents/node_modules/wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "inBundle": true, "license": "ISC", "optional": true, "dependencies": { "string-width": "^1.0.2 || 2" } }, "node_modules/fsevents/node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "inBundle": true, "license": "ISC", "optional": true }, "node_modules/fsevents/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "inBundle": true, "license": "ISC", "optional": true }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, "node_modules/gensync": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dependencies": { "pump": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "engines": { "node": ">=0.10.0" } }, "node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, "engines": { "node": "*" } }, "node_modules/glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dependencies": { "is-glob": "^3.1.0", "path-dirname": "^1.0.0" } }, "node_modules/glob-parent/node_modules/is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dependencies": { "is-extglob": "^2.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", "dependencies": { "global-prefix": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/global-modules/node_modules/global-prefix": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", "dependencies": { "ini": "^1.3.5", "kind-of": "^6.0.2", "which": "^1.3.1" }, "engines": { "node": ">=6" } }, "node_modules/global-prefix": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", "dependencies": { "expand-tilde": "^2.0.2", "homedir-polyfill": "^1.0.1", "ini": "^1.3.4", "is-windows": "^1.0.1", "which": "^1.2.14" }, "engines": { "node": ">=0.10.0" } }, "node_modules/globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", "engines": { "node": ">=0.10.0" } }, "node_modules/graceful-fs": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, "node_modules/has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "dependencies": { "ansi-regex": "^2.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "engines": { "node": ">=4" } }, "node_modules/has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true, "engines": { "node": ">= 0.4" } }, "node_modules/has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", "dependencies": { "get-value": "^2.0.6", "has-values": "^1.0.0", "isobject": "^3.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/has-values": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", "dependencies": { "is-number": "^3.0.0", "kind-of": "^4.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/has-values/node_modules/kind-of": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", "dependencies": { "is-buffer": "^1.1.5" }, "engines": { "node": ">=0.10.0" } }, "node_modules/hash-base": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" }, "engines": { "node": ">=4" } }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "node_modules/home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", "dependencies": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", "dependencies": { "parse-passwd": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, "node_modules/ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "node_modules/iferr": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" }, "node_modules/import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", "dependencies": { "pkg-dir": "^3.0.0", "resolve-cwd": "^2.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" }, "engines": { "node": ">=6" } }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "engines": { "node": ">=0.8.19" } }, "node_modules/infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "engines": { "node": "*" } }, "node_modules/interpret": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", "engines": { "node": ">= 0.10" } }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "dependencies": { "loose-envify": "^1.0.0" } }, "node_modules/invert-kv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", "engines": { "node": ">=4" } }, "node_modules/is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dependencies": { "kind-of": "^3.0.2" }, "engines": { "node": ">=0.10.0" } }, "node_modules/is-accessor-descriptor/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dependencies": { "is-buffer": "^1.1.5" }, "engines": { "node": ">=0.10.0" } }, "node_modules/is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", "dependencies": { "binary-extensions": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "node_modules/is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dependencies": { "kind-of": "^3.0.2" }, "engines": { "node": ">=0.10.0" } }, "node_modules/is-data-descriptor/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dependencies": { "is-buffer": "^1.1.5" }, "engines": { "node": ">=0.10.0" } }, "node_modules/is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dependencies": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", "kind-of": "^5.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/is-descriptor/node_modules/kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", "engines": { "node": ">=0.10.0" } }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "engines": { "node": ">=0.10.0" } }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "engines": { "node": ">=0.10.0" } }, "node_modules/is-finite": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", "engines": { "node": ">=0.10.0" } }, "node_modules/is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "engines": { "node": ">=4" } }, "node_modules/is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", "dependencies": { "is-extglob": "^2.1.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dependencies": { "kind-of": "^3.0.2" }, "engines": { "node": ">=0.10.0" } }, "node_modules/is-number/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dependencies": { "is-buffer": "^1.1.5" }, "engines": { "node": ">=0.10.0" } }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dependencies": { "isobject": "^3.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "engines": { "node": ">=0.10.0" } }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "engines": { "node": ">=0.10.0" } }, "node_modules/is-wsl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", "engines": { "node": ">=4" } }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "engines": { "node": ">=0.10.0" } }, "node_modules/js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" }, "node_modules/jsesc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", "bin": { "jsesc": "bin/jsesc" } }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json5": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "engines": { "node": ">=0.10.0" } }, "node_modules/lcid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", "dependencies": { "invert-kv": "^2.0.0" }, "engines": { "node": ">=6" } }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/levenary": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz", "integrity": "sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==", "dev": true, "dependencies": { "leven": "^3.1.0" }, "engines": { "node": ">= 6" } }, "node_modules/linkify-it": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz", "integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==", "dependencies": { "uc.micro": "^1.0.1" } }, "node_modules/loader-runner": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", "engines": { "node": ">=4.3.0 <5.0.0 || >=5.10" } }, "node_modules/loader-utils": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^1.0.1" }, "engines": { "node": ">=4.0.0" } }, "node_modules/locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dependencies": { "yallist": "^3.0.2" } }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dependencies": { "pify": "^4.0.1", "semver": "^5.6.0" }, "engines": { "node": ">=6" } }, "node_modules/mamacro": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==" }, "node_modules/map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", "dependencies": { "p-defer": "^1.0.0" }, "engines": { "node": ">=6" } }, "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", "engines": { "node": ">=0.10.0" } }, "node_modules/map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", "dependencies": { "object-visit": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/markdown-it": { "version": "12.0.4", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.4.tgz", "integrity": "sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q==", "dependencies": { "argparse": "^2.0.1", "entities": "~2.1.0", "linkify-it": "^3.0.1", "mdurl": "^1.0.1", "uc.micro": "^1.0.5" }, "bin": { "markdown-it": "bin/markdown-it.js" } }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" }, "node_modules/mem": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", "dependencies": { "map-age-cleaner": "^0.1.1", "mimic-fn": "^2.0.0", "p-is-promise": "^2.0.0" }, "engines": { "node": ">=6" } }, "node_modules/memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", "dependencies": { "errno": "^0.1.3", "readable-stream": "^2.0.1" } }, "node_modules/micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dependencies": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", "braces": "^2.3.1", "define-property": "^2.0.2", "extend-shallow": "^3.0.2", "extglob": "^2.0.4", "fragment-cache": "^0.2.1", "kind-of": "^6.0.2", "nanomatch": "^1.2.9", "object.pick": "^1.3.0", "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.2" }, "engines": { "node": ">=0.10.0" } }, "node_modules/miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", "dependencies": { "bn.js": "^4.0.0", "brorand": "^1.0.1" }, "bin": { "miller-rabin": "bin/miller-rabin" } }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "engines": { "node": ">=6" } }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "node_modules/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dependencies": { "brace-expansion": "^1.1.7" }, "engines": { "node": "*" } }, "node_modules/minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "node_modules/mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", "dependencies": { "concat-stream": "^1.5.0", "duplexify": "^3.4.2", "end-of-stream": "^1.1.0", "flush-write-stream": "^1.0.0", "from2": "^2.1.0", "parallel-transform": "^1.1.0", "pump": "^3.0.0", "pumpify": "^1.3.3", "stream-each": "^1.1.0", "through2": "^2.0.0" }, "engines": { "node": ">=4.0.0" } }, "node_modules/mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dependencies": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/mixin-deep/node_modules/is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dependencies": { "is-plain-object": "^2.0.4" }, "engines": { "node": ">=0.10.0" } }, "node_modules/mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dependencies": { "minimist": "0.0.8" }, "bin": { "mkdirp": "bin/cmd.js" } }, "node_modules/mkdirp/node_modules/minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "node_modules/move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", "dependencies": { "aproba": "^1.1.1", "copy-concurrently": "^1.0.0", "fs-write-stream-atomic": "^1.0.8", "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.3" } }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "node_modules/nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", "optional": true }, "node_modules/nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", "dependencies": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", "define-property": "^2.0.2", "extend-shallow": "^3.0.2", "fragment-cache": "^0.2.1", "is-windows": "^1.0.2", "kind-of": "^6.0.2", "object.pick": "^1.3.0", "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/neo-async": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "node_modules/node-libs-browser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", "dependencies": { "assert": "^1.1.1", "browserify-zlib": "^0.2.0", "buffer": "^4.3.0", "console-browserify": "^1.1.0", "constants-browserify": "^1.0.0", "crypto-browserify": "^3.11.0", "domain-browser": "^1.1.1", "events": "^3.0.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "0.0.1", "process": "^0.11.10", "punycode": "^1.2.4", "querystring-es3": "^0.2.0", "readable-stream": "^2.3.3", "stream-browserify": "^2.0.1", "stream-http": "^2.7.2", "string_decoder": "^1.0.0", "timers-browserify": "^2.0.4", "tty-browserify": "0.0.0", "url": "^0.11.0", "util": "^0.11.0", "vm-browserify": "^1.0.1" } }, "node_modules/node-libs-browser/node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "node_modules/node-releases": { "version": "1.1.61", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.61.tgz", "integrity": "sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g==", "dev": true }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "engines": { "node": ">=0.10.0" } }, "node_modules/npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dependencies": { "path-key": "^2.0.0" }, "engines": { "node": ">=4" } }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "engines": { "node": ">=0.10.0" } }, "node_modules/object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", "dependencies": { "copy-descriptor": "^0.1.0", "define-property": "^0.2.5", "kind-of": "^3.0.3" }, "engines": { "node": ">=0.10.0" } }, "node_modules/object-copy/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dependencies": { "is-descriptor": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/object-copy/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dependencies": { "is-buffer": "^1.1.5" }, "engines": { "node": ">=0.10.0" } }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "engines": { "node": ">= 0.4" } }, "node_modules/object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", "dependencies": { "isobject": "^3.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/object.assign": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", "dev": true, "dependencies": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", "has-symbols": "^1.0.0", "object-keys": "^1.0.11" }, "engines": { "node": ">= 0.4" } }, "node_modules/object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", "dependencies": { "isobject": "^3.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dependencies": { "wrappy": "1" } }, "node_modules/orderedmap": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.1.tgz", "integrity": "sha512-3Ux8um0zXbVacKUkcytc0u3HgC0b0bBLT+I60r2J/En72cI0nZffqrA7Xtf2Hqs27j1g82llR5Mhbd0Z1XW4AQ==" }, "node_modules/os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" }, "node_modules/os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "engines": { "node": ">=0.10.0" } }, "node_modules/os-locale": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", "dependencies": { "execa": "^1.0.0", "lcid": "^2.0.0", "mem": "^4.0.0" }, "engines": { "node": ">=6" } }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "engines": { "node": ">=0.10.0" } }, "node_modules/p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", "engines": { "node": ">=4" } }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "engines": { "node": ">=4" } }, "node_modules/p-is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", "engines": { "node": ">=6" } }, "node_modules/p-limit": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", "dependencies": { "p-try": "^2.0.0" }, "engines": { "node": ">=6" } }, "node_modules/p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dependencies": { "p-limit": "^2.0.0" }, "engines": { "node": ">=6" } }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "engines": { "node": ">=6" } }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "node_modules/parallel-transform": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", "dependencies": { "cyclist": "^1.0.1", "inherits": "^2.0.3", "readable-stream": "^2.1.5" } }, "node_modules/parse-asn1": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", "dependencies": { "asn1.js": "^4.0.0", "browserify-aes": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", "pbkdf2": "^3.0.3", "safe-buffer": "^5.1.1" } }, "node_modules/parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", "engines": { "node": ">=0.10.0" } }, "node_modules/pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", "engines": { "node": ">=0.10.0" } }, "node_modules/path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==" }, "node_modules/path-dirname": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" }, "node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "engines": { "node": ">=4" } }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "engines": { "node": ">=4" } }, "node_modules/path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, "node_modules/pbkdf2": { "version": "3.0.17", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", "dependencies": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", "ripemd160": "^2.0.1", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" }, "engines": { "node": ">=0.12" } }, "node_modules/pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "engines": { "node": ">=6" } }, "node_modules/pkg-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", "dependencies": { "find-up": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "engines": { "node": ">=0.10.0" } }, "node_modules/prettier": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", "dev": true, "bin": { "prettier": "bin-prettier.js" }, "engines": { "node": ">=10.13.0" } }, "node_modules/private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", "engines": { "node": ">= 0.6" } }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", "engines": { "node": ">= 0.6.0" } }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" }, "node_modules/prosemirror-commands": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.1.3.tgz", "integrity": "sha512-YVbKwTR4likoyhuwIUC9egbzHvnFrFUNbiesB0DB/HZ8hBcopQ42Tb4KGlYrS3n+pNDTFObN73CLFY6mYLN2IQ==", "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "node_modules/prosemirror-dropcursor": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.3.2.tgz", "integrity": "sha512-4c94OUGyobGnwcQI70OXyMhE/9T4aTgjU+CHxkd5c7D+jH/J0mKM/lk+jneFVKt7+E4/M0D9HzRPifu8U28Thw==", "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "node_modules/prosemirror-example-setup": { "version": "1.1.2", "resolved": "git+ssh://git@github.com/writefreely/prosemirror-example-setup.git#aaea7d62ebc2fa769139faf8a097449441c8237c", "license": "MIT", "dependencies": { "prosemirror-commands": "^1.0.0", "prosemirror-dropcursor": "^1.0.0", "prosemirror-gapcursor": "^1.0.0", "prosemirror-history": "^1.0.0", "prosemirror-inputrules": "^1.0.0", "prosemirror-keymap": "^1.0.0", "prosemirror-menu": "^1.0.0", "prosemirror-schema-list": "^1.0.0", "prosemirror-state": "^1.0.0" } }, "node_modules/prosemirror-gapcursor": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.1.3.tgz", "integrity": "sha512-/lgWvt2AdHjsM6oEsF65z0lhdQJGl6sQSfXSOX8/xjpd8ycfOolhgKZd4TPYpikwnh85JF4l5eIyiFZsl/RQQA==", "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "node_modules/prosemirror-history": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.1.3.tgz", "integrity": "sha512-zGDotijea+vnfnyyUGyiy1wfOQhf0B/b6zYcCouBV8yo6JmrE9X23M5q7Nf/nATywEZbgRLG70R4DmfSTC+gfg==", "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "rope-sequence": "^1.3.0" } }, "node_modules/prosemirror-inputrules": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.1.2.tgz", "integrity": "sha512-Ja5Z3BWestlHYGvtSGqyvxMeB8QEuBjlHM8YnKtLGUXMDp965qdDV4goV8lJb17kIWHk7e7JNj6Catuoa3302g==", "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "node_modules/prosemirror-keymap": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.1.4.tgz", "integrity": "sha512-Al8cVUOnDFL4gcI5IDlG6xbZ0aOD/i3B17VT+1JbHWDguCgt/lBHVTHUBcKvvbSg6+q/W4Nj1Fu6bwZSca3xjg==", "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "node_modules/prosemirror-markdown": { "version": "1.5.1", "resolved": "git+ssh://git@github.com/writefreely/prosemirror-markdown.git#809d0a444cf4d366d7c0c350d881df1b55d172f2", "license": "MIT", "dependencies": { "markdown-it": "^10.0.0", "prosemirror-model": "^1.0.0" } }, "node_modules/prosemirror-markdown/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dependencies": { "sprintf-js": "~1.0.2" } }, "node_modules/prosemirror-markdown/node_modules/entities": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==" }, "node_modules/prosemirror-markdown/node_modules/linkify-it": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", "dependencies": { "uc.micro": "^1.0.1" } }, "node_modules/prosemirror-markdown/node_modules/markdown-it": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", "dependencies": { "argparse": "^1.0.7", "entities": "~2.0.0", "linkify-it": "^2.0.0", "mdurl": "^1.0.1", "uc.micro": "^1.0.5" }, "bin": { "markdown-it": "bin/markdown-it.js" } }, "node_modules/prosemirror-menu": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.1.3.tgz", "integrity": "sha512-G+XiTdiVi1uGeiBR1OqtiVP3p0XyCKec4DYLk75G7J2R/N8OThHt/1StJYDlXPNzqYsQFZfQyPp/UwOL8hD6AA==", "dependencies": { "crel": "^4.2.0", "prosemirror-commands": "^1.0.0", "prosemirror-history": "^1.0.0", "prosemirror-state": "^1.0.0" } }, "node_modules/prosemirror-model": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.9.1.tgz", "integrity": "sha512-Qblh8pm1c7Ll64sYLauwwzjimo/tFg1zW3Q3IWhKRhvfOEgRKqa6dC5pRrAa+XHOIjBFEYrqbi52J5bqA2dV8Q==", "dependencies": { "orderedmap": "^1.1.0" } }, "node_modules/prosemirror-schema-list": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.1.2.tgz", "integrity": "sha512-dgM9PwtM4twa5WsgSYMB+J8bwjnR43DAD3L9MsR9rKm/nZR5Y85xcjB7gusVMSsbQ2NomMZF03RE6No6mTnclQ==", "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "node_modules/prosemirror-state": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.3.2.tgz", "integrity": "sha512-t/JqE3aR0SV9QrzFVkAXsQwsgrQBNs/BDbcFH20RssW0xauqNNdjTXxy/J/kM7F+0zYi6+BRmz7cMMQQFU3mwQ==", "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "node_modules/prosemirror-transform": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.2.4.tgz", "integrity": "sha512-0A668uf0EN89L9O9brE05kHcqp7FHmT5YN7Tom58Kj926QqOBs7iNRHDLWxrSaQB5MNZtzDOD9T3EyJ88YDcBg==", "dependencies": { "prosemirror-model": "^1.0.0" } }, "node_modules/prosemirror-view": { "version": "1.14.2", "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.14.2.tgz", "integrity": "sha512-9yPVH6OLyaEraHjWHbSk2DB0R/1TsEE6AA1LI+vmCypXXA+zTzNrktUFzBhSJHehXDoEJcQfnl1Wdp5GPSh2+g==", "dependencies": { "prosemirror-model": "^1.1.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" }, "node_modules/public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" } }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "node_modules/pumpify": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "dependencies": { "duplexify": "^3.6.0", "inherits": "^2.0.3", "pump": "^2.0.0" } }, "node_modules/pumpify/node_modules/pump": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "engines": { "node": ">=6" } }, "node_modules/querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", "engines": { "node": ">=0.4.x" } }, "node_modules/querystring-es3": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "engines": { "node": ">=0.4.x" } }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dependencies": { "safe-buffer": "^5.1.0" } }, "node_modules/randomfill": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", "dependencies": { "randombytes": "^2.0.5", "safe-buffer": "^5.1.0" } }, "node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "node_modules/readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", "dependencies": { "graceful-fs": "^4.1.11", "micromatch": "^3.1.10", "readable-stream": "^2.0.2" }, "engines": { "node": ">=0.10" } }, "node_modules/regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" }, "node_modules/regenerate-unicode-properties": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", "dev": true, "dependencies": { "regenerate": "^1.4.0" }, "engines": { "node": ">=4" } }, "node_modules/regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, "node_modules/regenerator-transform": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", "dependencies": { "babel-runtime": "^6.18.0", "babel-types": "^6.19.0", "private": "^0.1.6" } }, "node_modules/regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", "dependencies": { "extend-shallow": "^3.0.2", "safe-regex": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/regexpu-core": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", "dependencies": { "regenerate": "^1.2.1", "regjsgen": "^0.2.0", "regjsparser": "^0.1.4" } }, "node_modules/regjsgen": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" }, "node_modules/regjsparser": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", "dependencies": { "jsesc": "~0.5.0" }, "bin": { "regjsparser": "bin/parser" } }, "node_modules/regjsparser/node_modules/jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "bin": { "jsesc": "bin/jsesc" } }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, "node_modules/repeat-element": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", "engines": { "node": ">=0.10.0" } }, "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "engines": { "node": ">=0.10" } }, "node_modules/repeating": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dependencies": { "is-finite": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "engines": { "node": ">=0.10.0" } }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "node_modules/resolve": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", "dev": true, "dependencies": { "path-parse": "^1.0.6" } }, "node_modules/resolve-cwd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", "dependencies": { "resolve-from": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/resolve-dir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", "dependencies": { "expand-tilde": "^2.0.0", "global-modules": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/resolve-dir/node_modules/global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", "dependencies": { "global-prefix": "^1.0.1", "is-windows": "^1.0.1", "resolve-dir": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/resolve-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", "engines": { "node": ">=4" } }, "node_modules/resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" }, "node_modules/ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "engines": { "node": ">=0.12" } }, "node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "node_modules/ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, "node_modules/rope-sequence": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.2.tgz", "integrity": "sha512-ku6MFrwEVSVmXLvy3dYph3LAMNS0890K7fabn+0YIRQ2T96T9F4gkFf0vf0WW0JUraNWwGRtInEpH7yO4tbQZg==" }, "node_modules/run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", "dependencies": { "aproba": "^1.1.1" } }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dependencies": { "ret": "~0.1.10" } }, "node_modules/schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", "dependencies": { "ajv": "^6.1.0", "ajv-errors": "^1.0.0", "ajv-keywords": "^3.1.0" }, "engines": { "node": ">= 4" } }, "node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "bin": { "semver": "bin/semver" } }, "node_modules/serialize-javascript": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==" }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dependencies": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", "is-plain-object": "^2.0.3", "split-string": "^3.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/set-value/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dependencies": { "is-extendable": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "node_modules/sha.js": { "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" }, "bin": { "sha.js": "bin.js" } }, "node_modules/shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "dependencies": { "shebang-regex": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "engines": { "node": ">=0.10.0" } }, "node_modules/signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "node_modules/slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", "engines": { "node": ">=0.10.0" } }, "node_modules/snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", "dependencies": { "base": "^0.11.1", "debug": "^2.2.0", "define-property": "^0.2.5", "extend-shallow": "^2.0.1", "map-cache": "^0.2.2", "source-map": "^0.5.6", "source-map-resolve": "^0.5.0", "use": "^3.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/snapdragon-node": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", "dependencies": { "define-property": "^1.0.0", "isobject": "^3.0.0", "snapdragon-util": "^3.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/snapdragon-node/node_modules/define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dependencies": { "is-descriptor": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/snapdragon-node/node_modules/is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dependencies": { "kind-of": "^6.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/snapdragon-node/node_modules/is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dependencies": { "kind-of": "^6.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/snapdragon-node/node_modules/is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dependencies": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" }, "engines": { "node": ">=0.10.0" } }, "node_modules/snapdragon-util": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", "dependencies": { "kind-of": "^3.2.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/snapdragon-util/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dependencies": { "is-buffer": "^1.1.5" }, "engines": { "node": ">=0.10.0" } }, "node_modules/snapdragon/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dependencies": { "is-descriptor": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/snapdragon/node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dependencies": { "is-extendable": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", "dependencies": { "atob": "^2.1.2", "decode-uri-component": "^0.2.0", "resolve-url": "^0.2.1", "source-map-url": "^0.4.0", "urix": "^0.1.0" } }, "node_modules/source-map-support": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, "node_modules/split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", "dependencies": { "extend-shallow": "^3.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "node_modules/ssri": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", "dependencies": { "figgy-pudding": "^3.5.1" } }, "node_modules/static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", "dependencies": { "define-property": "^0.2.5", "object-copy": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/static-extend/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dependencies": { "is-descriptor": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", "dependencies": { "inherits": "~2.0.1", "readable-stream": "^2.0.2" } }, "node_modules/stream-each": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", "dependencies": { "end-of-stream": "^1.1.0", "stream-shift": "^1.0.0" } }, "node_modules/stream-http": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", "dependencies": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.1", "readable-stream": "^2.3.6", "to-arraybuffer": "^1.0.0", "xtend": "^4.0.0" } }, "node_modules/stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dependencies": { "safe-buffer": "~5.1.0" } }, "node_modules/string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dependencies": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" }, "engines": { "node": ">=6" } }, "node_modules/string-width/node_modules/ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "engines": { "node": ">=6" } }, "node_modules/string-width/node_modules/strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dependencies": { "ansi-regex": "^4.1.0" }, "engines": { "node": ">=6" } }, "node_modules/strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dependencies": { "ansi-regex": "^2.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "engines": { "node": ">=0.10.0" } }, "node_modules/supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", "engines": { "node": ">=0.8.0" } }, "node_modules/tapable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "engines": { "node": ">=6" } }, "node_modules/terser": { "version": "4.6.6", "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.6.tgz", "integrity": "sha512-4lYPyeNmstjIIESr/ysHg2vUPRGf2tzF9z2yYwnowXVuVzLEamPN1Gfrz7f8I9uEPuHcbFlW4PLIAsJoxXyJ1g==", "dependencies": { "commander": "^2.20.0", "source-map": "~0.6.1", "source-map-support": "~0.5.12" }, "bin": { "terser": "bin/terser" }, "engines": { "node": ">=6.0.0" } }, "node_modules/terser-webpack-plugin": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", "dependencies": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", "serialize-javascript": "^2.1.2", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", "worker-farm": "^1.7.0" }, "engines": { "node": ">= 6.9.0" } }, "node_modules/terser-webpack-plugin/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "engines": { "node": ">=0.10.0" } }, "node_modules/terser/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "engines": { "node": ">=0.10.0" } }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "node_modules/timers-browserify": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", "dependencies": { "setimmediate": "^1.0.4" }, "engines": { "node": ">=0.6.0" } }, "node_modules/to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" }, "node_modules/to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", "engines": { "node": ">=0.10.0" } }, "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", "dependencies": { "kind-of": "^3.0.2" }, "engines": { "node": ">=0.10.0" } }, "node_modules/to-object-path/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dependencies": { "is-buffer": "^1.1.5" }, "engines": { "node": ">=0.10.0" } }, "node_modules/to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", "dependencies": { "define-property": "^2.0.2", "extend-shallow": "^3.0.2", "regex-not": "^1.0.2", "safe-regex": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dependencies": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "engines": { "node": ">=0.10.0" } }, "node_modules/tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" }, "node_modules/tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/unicode-match-property-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", "dev": true, "dependencies": { "unicode-canonical-property-names-ecmascript": "^1.0.4", "unicode-property-aliases-ecmascript": "^1.0.4" }, "engines": { "node": ">=4" } }, "node_modules/unicode-match-property-value-ecmascript": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/unicode-property-aliases-ecmascript": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dependencies": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", "set-value": "^2.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", "dependencies": { "unique-slug": "^2.0.0" } }, "node_modules/unique-slug": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", "dependencies": { "imurmurhash": "^0.1.4" } }, "node_modules/unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", "dependencies": { "has-value": "^0.3.1", "isobject": "^3.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/unset-value/node_modules/has-value": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", "dependencies": { "get-value": "^2.0.3", "has-values": "^0.1.4", "isobject": "^2.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", "dependencies": { "isarray": "1.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/unset-value/node_modules/has-values": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", "engines": { "node": ">=0.10.0" } }, "node_modules/upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "engines": { "node": ">=4", "yarn": "*" } }, "node_modules/uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "dependencies": { "punycode": "^2.1.0" } }, "node_modules/urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" }, "node_modules/url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "node_modules/url/node_modules/punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" }, "node_modules/use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "engines": { "node": ">=0.10.0" } }, "node_modules/util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "dependencies": { "inherits": "2.0.3" } }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "node_modules/util/node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "node_modules/v8-compile-cache": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==" }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, "node_modules/w3c-keyname": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz", "integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==" }, "node_modules/watchpack": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", "dependencies": { "chokidar": "^2.0.2", "graceful-fs": "^4.1.2", "neo-async": "^2.5.0" } }, "node_modules/webpack": { "version": "4.42.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.0.tgz", "integrity": "sha512-EzJRHvwQyBiYrYqhyjW9AqM90dE4+s1/XtCfn7uWg6cS72zH+2VPFAlsnW0+W0cDi0XRjNKUMoJtpSi50+Ph6w==", "dependencies": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-module-context": "1.8.5", "@webassemblyjs/wasm-edit": "1.8.5", "@webassemblyjs/wasm-parser": "1.8.5", "acorn": "^6.2.1", "ajv": "^6.10.2", "ajv-keywords": "^3.4.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^4.1.0", "eslint-scope": "^4.0.3", "json-parse-better-errors": "^1.0.2", "loader-runner": "^2.4.0", "loader-utils": "^1.2.3", "memory-fs": "^0.4.1", "micromatch": "^3.1.10", "mkdirp": "^0.5.1", "neo-async": "^2.6.1", "node-libs-browser": "^2.2.1", "schema-utils": "^1.0.0", "tapable": "^1.1.3", "terser-webpack-plugin": "^1.4.3", "watchpack": "^1.6.0", "webpack-sources": "^1.4.1" }, "bin": { "webpack": "bin/webpack.js" }, "engines": { "node": ">=6.11.5" } }, "node_modules/webpack-cli": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.11.tgz", "integrity": "sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g==", "dependencies": { "chalk": "2.4.2", "cross-spawn": "6.0.5", "enhanced-resolve": "4.1.0", "findup-sync": "3.0.0", "global-modules": "2.0.0", "import-local": "2.0.0", "interpret": "1.2.0", "loader-utils": "1.2.3", "supports-color": "6.1.0", "v8-compile-cache": "2.0.3", "yargs": "13.2.4" }, "bin": { "webpack-cli": "bin/cli.js" }, "engines": { "node": ">=6.11.5" } }, "node_modules/webpack-cli/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dependencies": { "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, "node_modules/webpack-cli/node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" }, "engines": { "node": ">=4" } }, "node_modules/webpack-cli/node_modules/chalk/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dependencies": { "has-flag": "^3.0.0" }, "engines": { "node": ">=4" } }, "node_modules/webpack-cli/node_modules/emojis-list": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", "engines": { "node": ">= 0.10" } }, "node_modules/webpack-cli/node_modules/enhanced-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", "dependencies": { "graceful-fs": "^4.1.2", "memory-fs": "^0.4.0", "tapable": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/webpack-cli/node_modules/loader-utils": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^2.0.0", "json5": "^1.0.1" }, "engines": { "node": ">=4.0.0" } }, "node_modules/webpack-cli/node_modules/supports-color": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", "dependencies": { "has-flag": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/webpack-sources": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", "dependencies": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" } }, "node_modules/webpack-sources/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "engines": { "node": ">=0.10.0" } }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "bin/which" } }, "node_modules/which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "node_modules/worker-farm": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", "dependencies": { "errno": "~0.1.7" } }, "node_modules/wrap-ansi": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "dependencies": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", "strip-ansi": "^5.0.0" }, "engines": { "node": ">=6" } }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "engines": { "node": ">=6" } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dependencies": { "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dependencies": { "ansi-regex": "^4.1.0" }, "engines": { "node": ">=6" } }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "engines": { "node": ">=0.4" } }, "node_modules/y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yargs": { "version": "13.2.4", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", "dependencies": { "cliui": "^5.0.0", "find-up": "^3.0.0", "get-caller-file": "^2.0.1", "os-locale": "^3.1.0", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^13.1.0" } }, "node_modules/yargs-parser": { "version": "13.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } } }, "dependencies": { "@babel/code-frame": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", "dev": true, "requires": { "@babel/highlight": "^7.8.3" } }, "@babel/compat-data": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz", "integrity": "sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ==", "dev": true, "requires": { "browserslist": "^4.12.0", "invariant": "^2.2.4", "semver": "^5.5.0" } }, "@babel/core": { "version": "7.8.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.7.tgz", "integrity": "sha512-rBlqF3Yko9cynC5CCFy6+K/w2N+Sq/ff2BPy+Krp7rHlABIr5epbA7OxVeKoMHB39LZOp1UY5SuLjy6uWi35yA==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", "@babel/generator": "^7.8.7", "@babel/helpers": "^7.8.4", "@babel/parser": "^7.8.7", "@babel/template": "^7.8.6", "@babel/traverse": "^7.8.6", "@babel/types": "^7.8.7", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.1", "json5": "^2.1.0", "lodash": "^4.17.13", "resolve": "^1.3.2", "semver": "^5.4.1", "source-map": "^0.5.0" }, "dependencies": { "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { "ms": "^2.1.1" } }, "json5": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", "dev": true, "requires": { "minimist": "^1.2.0" } }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } }, "@babel/generator": { "version": "7.8.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.7.tgz", "integrity": "sha512-DQwjiKJqH4C3qGiyQCAExJHoZssn49JTMJgZ8SANGgVFdkupcUhLOdkAeoC6kmHZCPfoDG5M0b6cFlSN5wW7Ew==", "dev": true, "requires": { "@babel/types": "^7.8.7", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" }, "dependencies": { "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true } } }, "@babel/helper-annotate-as-pure": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { "@babel/types": "^7.10.4" }, "dependencies": { "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-builder-binary-assignment-operator-visitor": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", "dev": true, "requires": { "@babel/helper-explode-assignable-expression": "^7.10.4", "@babel/types": "^7.10.4" }, "dependencies": { "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-compilation-targets": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz", "integrity": "sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ==", "dev": true, "requires": { "@babel/compat-data": "^7.10.4", "browserslist": "^4.12.0", "invariant": "^2.2.4", "levenary": "^1.1.1", "semver": "^5.5.0" } }, "@babel/helper-create-class-features-plugin": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz", "integrity": "sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==", "dev": true, "requires": { "@babel/helper-function-name": "^7.10.4", "@babel/helper-member-expression-to-functions": "^7.10.5", "@babel/helper-optimise-call-expression": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-replace-supers": "^7.10.4", "@babel/helper-split-export-declaration": "^7.10.4" }, "dependencies": { "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { "@babel/types": "^7.11.0" } }, "@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { "has-flag": "^3.0.0" } }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-create-regexp-features-plugin": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz", "integrity": "sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-regex": "^7.10.4", "regexpu-core": "^4.7.0" }, "dependencies": { "jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true }, "regexpu-core": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", "integrity": "sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==", "dev": true, "requires": { "regenerate": "^1.4.0", "regenerate-unicode-properties": "^8.2.0", "regjsgen": "^0.5.1", "regjsparser": "^0.6.4", "unicode-match-property-ecmascript": "^1.0.4", "unicode-match-property-value-ecmascript": "^1.2.0" } }, "regjsgen": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", "dev": true }, "regjsparser": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", "dev": true, "requires": { "jsesc": "~0.5.0" } } } }, "@babel/helper-define-map": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", "dev": true, "requires": { "@babel/helper-function-name": "^7.10.4", "@babel/types": "^7.10.5", "lodash": "^4.17.19" }, "dependencies": { "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { "@babel/types": "^7.10.4" } }, "@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { "has-flag": "^3.0.0" } }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-explode-assignable-expression": { "version": "7.11.4", "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz", "integrity": "sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ==", "dev": true, "requires": { "@babel/types": "^7.10.4" }, "dependencies": { "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-function-name": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.8.3", "@babel/template": "^7.8.3", "@babel/types": "^7.8.3" } }, "@babel/helper-get-function-arity": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", "dev": true, "requires": { "@babel/types": "^7.8.3" } }, "@babel/helper-hoist-variables": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", "dev": true, "requires": { "@babel/types": "^7.10.4" }, "dependencies": { "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-member-expression-to-functions": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", "dev": true, "requires": { "@babel/types": "^7.11.0" }, "dependencies": { "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-module-imports": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { "@babel/types": "^7.10.4" }, "dependencies": { "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-module-transforms": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", "dev": true, "requires": { "@babel/helper-module-imports": "^7.10.4", "@babel/helper-replace-supers": "^7.10.4", "@babel/helper-simple-access": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", "@babel/template": "^7.10.4", "@babel/types": "^7.11.0", "lodash": "^4.17.19" }, "dependencies": { "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/helper-split-export-declaration": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { "@babel/types": "^7.11.0" } }, "@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { "has-flag": "^3.0.0" } }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-optimise-call-expression": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", "dev": true, "requires": { "@babel/types": "^7.10.4" }, "dependencies": { "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-plugin-utils": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", "dev": true }, "@babel/helper-regex": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz", "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==", "dev": true, "requires": { "lodash": "^4.17.19" }, "dependencies": { "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true } } }, "@babel/helper-remap-async-to-generator": { "version": "7.11.4", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz", "integrity": "sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-wrap-function": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { "has-flag": "^3.0.0" } }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-replace-supers": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", "dev": true, "requires": { "@babel/helper-member-expression-to-functions": "^7.10.4", "@babel/helper-optimise-call-expression": "^7.10.4", "@babel/traverse": "^7.10.4", "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/generator": { "version": "7.11.6", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", "dev": true, "requires": { "@babel/types": "^7.11.5", "jsesc": "^2.5.1", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { "@babel/types": "^7.11.0" } }, "@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/traverse": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.11.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", "@babel/parser": "^7.11.5", "@babel/types": "^7.11.5", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { "ms": "^2.1.1" } }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { "has-flag": "^3.0.0" } }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-simple-access": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", "dev": true, "requires": { "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { "has-flag": "^3.0.0" } }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-skip-transparent-expression-wrappers": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz", "integrity": "sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==", "dev": true, "requires": { "@babel/types": "^7.11.0" }, "dependencies": { "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helper-split-export-declaration": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", "dev": true, "requires": { "@babel/types": "^7.8.3" } }, "@babel/helper-validator-identifier": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/helper-wrap-function": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz", "integrity": "sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==", "dev": true, "requires": { "@babel/helper-function-name": "^7.10.4", "@babel/template": "^7.10.4", "@babel/traverse": "^7.10.4", "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/generator": { "version": "7.11.6", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", "dev": true, "requires": { "@babel/types": "^7.11.5", "jsesc": "^2.5.1", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { "@babel/types": "^7.11.0" } }, "@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/traverse": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.11.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", "@babel/parser": "^7.11.5", "@babel/types": "^7.11.5", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { "ms": "^2.1.1" } }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { "has-flag": "^3.0.0" } }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/helpers": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.4.tgz", "integrity": "sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w==", "dev": true, "requires": { "@babel/template": "^7.8.3", "@babel/traverse": "^7.8.4", "@babel/types": "^7.8.3" } }, "@babel/highlight": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", "dev": true, "requires": { "chalk": "^2.0.0", "esutils": "^2.0.2", "js-tokens": "^4.0.0" }, "dependencies": { "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { "has-flag": "^3.0.0" } } } }, "@babel/parser": { "version": "7.8.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz", "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==", "dev": true }, "@babel/plugin-proposal-async-generator-functions": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz", "integrity": "sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-remap-async-to-generator": "^7.10.4", "@babel/plugin-syntax-async-generators": "^7.8.0" } }, "@babel/plugin-proposal-class-properties": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz", "integrity": "sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==", "dev": true, "requires": { "@babel/helper-create-class-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-proposal-dynamic-import": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz", "integrity": "sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-dynamic-import": "^7.8.0" } }, "@babel/plugin-proposal-export-namespace-from": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz", "integrity": "sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" } }, "@babel/plugin-proposal-json-strings": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz", "integrity": "sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.0" } }, "@babel/plugin-proposal-logical-assignment-operators": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz", "integrity": "sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" } }, "@babel/plugin-proposal-nullish-coalescing-operator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz", "integrity": "sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" } }, "@babel/plugin-proposal-numeric-separator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz", "integrity": "sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-numeric-separator": "^7.10.4" } }, "@babel/plugin-proposal-object-rest-spread": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz", "integrity": "sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", "@babel/plugin-transform-parameters": "^7.10.4" } }, "@babel/plugin-proposal-optional-catch-binding": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz", "integrity": "sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" } }, "@babel/plugin-proposal-optional-chaining": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz", "integrity": "sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0", "@babel/plugin-syntax-optional-chaining": "^7.8.0" } }, "@babel/plugin-proposal-private-methods": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz", "integrity": "sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw==", "dev": true, "requires": { "@babel/helper-create-class-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-proposal-unicode-property-regex": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz", "integrity": "sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==", "dev": true, "requires": { "@babel/helper-create-regexp-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } }, "@babel/plugin-syntax-class-properties": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } }, "@babel/plugin-syntax-export-namespace-from": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" } }, "@babel/plugin-syntax-json-strings": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } }, "@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } }, "@babel/plugin-syntax-numeric-separator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-object-rest-spread": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } }, "@babel/plugin-syntax-optional-catch-binding": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } }, "@babel/plugin-syntax-optional-chaining": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } }, "@babel/plugin-syntax-top-level-await": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz", "integrity": "sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-arrow-functions": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz", "integrity": "sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-async-to-generator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz", "integrity": "sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==", "dev": true, "requires": { "@babel/helper-module-imports": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-remap-async-to-generator": "^7.10.4" } }, "@babel/plugin-transform-block-scoped-functions": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz", "integrity": "sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-block-scoping": { "version": "7.11.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz", "integrity": "sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-classes": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz", "integrity": "sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-define-map": "^7.10.4", "@babel/helper-function-name": "^7.10.4", "@babel/helper-optimise-call-expression": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-replace-supers": "^7.10.4", "@babel/helper-split-export-declaration": "^7.10.4", "globals": "^11.1.0" }, "dependencies": { "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { "@babel/types": "^7.11.0" } }, "@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { "has-flag": "^3.0.0" } }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/plugin-transform-computed-properties": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz", "integrity": "sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-destructuring": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz", "integrity": "sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-dotall-regex": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz", "integrity": "sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==", "dev": true, "requires": { "@babel/helper-create-regexp-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-duplicate-keys": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz", "integrity": "sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-exponentiation-operator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz", "integrity": "sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==", "dev": true, "requires": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-for-of": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz", "integrity": "sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz", "integrity": "sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==", "dev": true, "requires": { "@babel/helper-function-name": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/template": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { "@babel/types": "^7.10.4" } }, "@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/parser": "^7.10.4", "@babel/types": "^7.10.4" } }, "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { "has-flag": "^3.0.0" } }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/plugin-transform-literals": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz", "integrity": "sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-member-expression-literals": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz", "integrity": "sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-modules-amd": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz", "integrity": "sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==", "dev": true, "requires": { "@babel/helper-module-transforms": "^7.10.5", "@babel/helper-plugin-utils": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-commonjs": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz", "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==", "dev": true, "requires": { "@babel/helper-module-transforms": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-simple-access": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-systemjs": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz", "integrity": "sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==", "dev": true, "requires": { "@babel/helper-hoist-variables": "^7.10.4", "@babel/helper-module-transforms": "^7.10.5", "@babel/helper-plugin-utils": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-umd": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz", "integrity": "sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==", "dev": true, "requires": { "@babel/helper-module-transforms": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-named-capturing-groups-regex": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz", "integrity": "sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==", "dev": true, "requires": { "@babel/helper-create-regexp-features-plugin": "^7.10.4" } }, "@babel/plugin-transform-new-target": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz", "integrity": "sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-object-super": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz", "integrity": "sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-replace-supers": "^7.10.4" } }, "@babel/plugin-transform-parameters": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz", "integrity": "sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==", "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/helper-get-function-arity": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { "@babel/types": "^7.10.4" } }, "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/plugin-transform-property-literals": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz", "integrity": "sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-regenerator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz", "integrity": "sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==", "dev": true, "requires": { "regenerator-transform": "^0.14.2" }, "dependencies": { "regenerator-transform": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", "dev": true, "requires": { "@babel/runtime": "^7.8.4" } } } }, "@babel/plugin-transform-reserved-words": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz", "integrity": "sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-shorthand-properties": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz", "integrity": "sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-spread": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz", "integrity": "sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0" } }, "@babel/plugin-transform-sticky-regex": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz", "integrity": "sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-regex": "^7.10.4" } }, "@babel/plugin-transform-template-literals": { "version": "7.10.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz", "integrity": "sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-typeof-symbol": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz", "integrity": "sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-unicode-escapes": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz", "integrity": "sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-unicode-regex": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz", "integrity": "sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==", "dev": true, "requires": { "@babel/helper-create-regexp-features-plugin": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/preset-env": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.5.tgz", "integrity": "sha512-kXqmW1jVcnB2cdueV+fyBM8estd5mlNfaQi6lwLgRwCby4edpavgbFhiBNjmWA3JpB/yZGSISa7Srf+TwxDQoA==", "dev": true, "requires": { "@babel/compat-data": "^7.11.0", "@babel/helper-compilation-targets": "^7.10.4", "@babel/helper-module-imports": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-proposal-async-generator-functions": "^7.10.4", "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-dynamic-import": "^7.10.4", "@babel/plugin-proposal-export-namespace-from": "^7.10.4", "@babel/plugin-proposal-json-strings": "^7.10.4", "@babel/plugin-proposal-logical-assignment-operators": "^7.11.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", "@babel/plugin-proposal-numeric-separator": "^7.10.4", "@babel/plugin-proposal-object-rest-spread": "^7.11.0", "@babel/plugin-proposal-optional-catch-binding": "^7.10.4", "@babel/plugin-proposal-optional-chaining": "^7.11.0", "@babel/plugin-proposal-private-methods": "^7.10.4", "@babel/plugin-proposal-unicode-property-regex": "^7.10.4", "@babel/plugin-syntax-async-generators": "^7.8.0", "@babel/plugin-syntax-class-properties": "^7.10.4", "@babel/plugin-syntax-dynamic-import": "^7.8.0", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", "@babel/plugin-syntax-json-strings": "^7.8.0", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", "@babel/plugin-syntax-optional-chaining": "^7.8.0", "@babel/plugin-syntax-top-level-await": "^7.10.4", "@babel/plugin-transform-arrow-functions": "^7.10.4", "@babel/plugin-transform-async-to-generator": "^7.10.4", "@babel/plugin-transform-block-scoped-functions": "^7.10.4", "@babel/plugin-transform-block-scoping": "^7.10.4", "@babel/plugin-transform-classes": "^7.10.4", "@babel/plugin-transform-computed-properties": "^7.10.4", "@babel/plugin-transform-destructuring": "^7.10.4", "@babel/plugin-transform-dotall-regex": "^7.10.4", "@babel/plugin-transform-duplicate-keys": "^7.10.4", "@babel/plugin-transform-exponentiation-operator": "^7.10.4", "@babel/plugin-transform-for-of": "^7.10.4", "@babel/plugin-transform-function-name": "^7.10.4", "@babel/plugin-transform-literals": "^7.10.4", "@babel/plugin-transform-member-expression-literals": "^7.10.4", "@babel/plugin-transform-modules-amd": "^7.10.4", "@babel/plugin-transform-modules-commonjs": "^7.10.4", "@babel/plugin-transform-modules-systemjs": "^7.10.4", "@babel/plugin-transform-modules-umd": "^7.10.4", "@babel/plugin-transform-named-capturing-groups-regex": "^7.10.4", "@babel/plugin-transform-new-target": "^7.10.4", "@babel/plugin-transform-object-super": "^7.10.4", "@babel/plugin-transform-parameters": "^7.10.4", "@babel/plugin-transform-property-literals": "^7.10.4", "@babel/plugin-transform-regenerator": "^7.10.4", "@babel/plugin-transform-reserved-words": "^7.10.4", "@babel/plugin-transform-shorthand-properties": "^7.10.4", "@babel/plugin-transform-spread": "^7.11.0", "@babel/plugin-transform-sticky-regex": "^7.10.4", "@babel/plugin-transform-template-literals": "^7.10.4", "@babel/plugin-transform-typeof-symbol": "^7.10.4", "@babel/plugin-transform-unicode-escapes": "^7.10.4", "@babel/plugin-transform-unicode-regex": "^7.10.4", "@babel/preset-modules": "^0.1.3", "@babel/types": "^7.11.5", "browserslist": "^4.12.0", "core-js-compat": "^3.6.2", "invariant": "^2.2.2", "levenary": "^1.1.1", "semver": "^5.5.0" }, "dependencies": { "@babel/types": { "version": "7.11.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@babel/preset-modules": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", "@babel/plugin-transform-dotall-regex": "^7.4.4", "@babel/types": "^7.4.4", "esutils": "^2.0.2" } }, "@babel/runtime": { "version": "7.11.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", "dev": true, "requires": { "regenerator-runtime": "^0.13.4" }, "dependencies": { "regenerator-runtime": { "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", "dev": true } } }, "@babel/template": { "version": "7.8.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", "@babel/parser": "^7.8.6", "@babel/types": "^7.8.6" } }, "@babel/traverse": { "version": "7.8.6", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", "@babel/generator": "^7.8.6", "@babel/helper-function-name": "^7.8.3", "@babel/helper-split-export-declaration": "^7.8.3", "@babel/parser": "^7.8.6", "@babel/types": "^7.8.6", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.13" }, "dependencies": { "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { "ms": "^2.1.1" } }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } }, "@babel/types": { "version": "7.8.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", "dev": true, "requires": { "esutils": "^2.0.2", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" }, "dependencies": { "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true } } }, "@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", "requires": { "@webassemblyjs/helper-module-context": "1.8.5", "@webassemblyjs/helper-wasm-bytecode": "1.8.5", "@webassemblyjs/wast-parser": "1.8.5" } }, "@webassemblyjs/floating-point-hex-parser": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==" }, "@webassemblyjs/helper-api-error": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==" }, "@webassemblyjs/helper-buffer": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==" }, "@webassemblyjs/helper-code-frame": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", "requires": { "@webassemblyjs/wast-printer": "1.8.5" } }, "@webassemblyjs/helper-fsm": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==" }, "@webassemblyjs/helper-module-context": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", "requires": { "@webassemblyjs/ast": "1.8.5", "mamacro": "^0.0.3" } }, "@webassemblyjs/helper-wasm-bytecode": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==" }, "@webassemblyjs/helper-wasm-section": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", "requires": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-buffer": "1.8.5", "@webassemblyjs/helper-wasm-bytecode": "1.8.5", "@webassemblyjs/wasm-gen": "1.8.5" } }, "@webassemblyjs/ieee754": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", "requires": { "@xtuc/ieee754": "^1.2.0" } }, "@webassemblyjs/leb128": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", "requires": { "@xtuc/long": "4.2.2" } }, "@webassemblyjs/utf8": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==" }, "@webassemblyjs/wasm-edit": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", "requires": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-buffer": "1.8.5", "@webassemblyjs/helper-wasm-bytecode": "1.8.5", "@webassemblyjs/helper-wasm-section": "1.8.5", "@webassemblyjs/wasm-gen": "1.8.5", "@webassemblyjs/wasm-opt": "1.8.5", "@webassemblyjs/wasm-parser": "1.8.5", "@webassemblyjs/wast-printer": "1.8.5" } }, "@webassemblyjs/wasm-gen": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", "requires": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-wasm-bytecode": "1.8.5", "@webassemblyjs/ieee754": "1.8.5", "@webassemblyjs/leb128": "1.8.5", "@webassemblyjs/utf8": "1.8.5" } }, "@webassemblyjs/wasm-opt": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", "requires": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-buffer": "1.8.5", "@webassemblyjs/wasm-gen": "1.8.5", "@webassemblyjs/wasm-parser": "1.8.5" } }, "@webassemblyjs/wasm-parser": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", "requires": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-api-error": "1.8.5", "@webassemblyjs/helper-wasm-bytecode": "1.8.5", "@webassemblyjs/ieee754": "1.8.5", "@webassemblyjs/leb128": "1.8.5", "@webassemblyjs/utf8": "1.8.5" } }, "@webassemblyjs/wast-parser": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", "requires": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/floating-point-hex-parser": "1.8.5", "@webassemblyjs/helper-api-error": "1.8.5", "@webassemblyjs/helper-code-frame": "1.8.5", "@webassemblyjs/helper-fsm": "1.8.5", "@xtuc/long": "4.2.2" } }, "@webassemblyjs/wast-printer": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", "requires": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/wast-parser": "1.8.5", "@xtuc/long": "4.2.2" } }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" }, "@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, "acorn": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" }, "ajv": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "ajv-errors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==" }, "ajv-keywords": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==" }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "requires": { "micromatch": "^3.1.4", "normalize-path": "^2.1.1" }, "dependencies": { "normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "requires": { "remove-trailing-separator": "^1.0.1" } } } }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" }, "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" }, "arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, "asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", "requires": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "assert": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", "requires": { "object-assign": "^4.1.1", "util": "0.10.3" }, "dependencies": { "inherits": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" }, "util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "requires": { "inherits": "2.0.1" } } } }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, "async-each": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", "requires": { "chalk": "^1.1.3", "esutils": "^2.0.2", "js-tokens": "^3.0.2" } }, "babel-core": { "version": "6.26.3", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", "requires": { "babel-code-frame": "^6.26.0", "babel-generator": "^6.26.0", "babel-helpers": "^6.24.1", "babel-messages": "^6.23.0", "babel-register": "^6.26.0", "babel-runtime": "^6.26.0", "babel-template": "^6.26.0", "babel-traverse": "^6.26.0", "babel-types": "^6.26.0", "babylon": "^6.18.0", "convert-source-map": "^1.5.1", "debug": "^2.6.9", "json5": "^0.5.1", "lodash": "^4.17.4", "minimatch": "^3.0.4", "path-is-absolute": "^1.0.1", "private": "^0.1.8", "slash": "^1.0.0", "source-map": "^0.5.7" }, "dependencies": { "json5": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" } } }, "babel-generator": { "version": "6.26.1", "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", "requires": { "babel-messages": "^6.23.0", "babel-runtime": "^6.26.0", "babel-types": "^6.26.0", "detect-indent": "^4.0.0", "jsesc": "^1.3.0", "lodash": "^4.17.4", "source-map": "^0.5.7", "trim-right": "^1.0.1" } }, "babel-helper-call-delegate": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", "requires": { "babel-helper-hoist-variables": "^6.24.1", "babel-runtime": "^6.22.0", "babel-traverse": "^6.24.1", "babel-types": "^6.24.1" } }, "babel-helper-define-map": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", "requires": { "babel-helper-function-name": "^6.24.1", "babel-runtime": "^6.26.0", "babel-types": "^6.26.0", "lodash": "^4.17.4" } }, "babel-helper-function-name": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", "requires": { "babel-helper-get-function-arity": "^6.24.1", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1", "babel-traverse": "^6.24.1", "babel-types": "^6.24.1" } }, "babel-helper-get-function-arity": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", "requires": { "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "babel-helper-hoist-variables": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", "requires": { "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "babel-helper-optimise-call-expression": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", "requires": { "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "babel-helper-regex": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", "requires": { "babel-runtime": "^6.26.0", "babel-types": "^6.26.0", "lodash": "^4.17.4" } }, "babel-helper-replace-supers": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", "requires": { "babel-helper-optimise-call-expression": "^6.24.1", "babel-messages": "^6.23.0", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1", "babel-traverse": "^6.24.1", "babel-types": "^6.24.1" } }, "babel-helpers": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", "requires": { "babel-runtime": "^6.22.0", "babel-template": "^6.24.1" } }, "babel-loader": { "version": "8.0.6", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz", "integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==", "dev": true, "requires": { "find-cache-dir": "^2.0.0", "loader-utils": "^1.0.2", "mkdirp": "^0.5.1", "pify": "^4.0.1" } }, "babel-messages": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", "requires": { "babel-runtime": "^6.22.0" } }, "babel-plugin-check-es2015-constants": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", "requires": { "babel-runtime": "^6.22.0" } }, "babel-plugin-dynamic-import-node": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", "dev": true, "requires": { "object.assign": "^4.1.0" } }, "babel-plugin-transform-es2015-arrow-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", "requires": { "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-block-scoped-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", "requires": { "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-block-scoping": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", "requires": { "babel-runtime": "^6.26.0", "babel-template": "^6.26.0", "babel-traverse": "^6.26.0", "babel-types": "^6.26.0", "lodash": "^4.17.4" } }, "babel-plugin-transform-es2015-classes": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", "requires": { "babel-helper-define-map": "^6.24.1", "babel-helper-function-name": "^6.24.1", "babel-helper-optimise-call-expression": "^6.24.1", "babel-helper-replace-supers": "^6.24.1", "babel-messages": "^6.23.0", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1", "babel-traverse": "^6.24.1", "babel-types": "^6.24.1" } }, "babel-plugin-transform-es2015-computed-properties": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", "requires": { "babel-runtime": "^6.22.0", "babel-template": "^6.24.1" } }, "babel-plugin-transform-es2015-destructuring": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", "requires": { "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-duplicate-keys": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", "requires": { "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "babel-plugin-transform-es2015-for-of": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", "requires": { "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-function-name": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", "requires": { "babel-helper-function-name": "^6.24.1", "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "babel-plugin-transform-es2015-literals": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", "requires": { "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-modules-amd": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", "requires": { "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1" } }, "babel-plugin-transform-es2015-modules-commonjs": { "version": "6.26.2", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", "requires": { "babel-plugin-transform-strict-mode": "^6.24.1", "babel-runtime": "^6.26.0", "babel-template": "^6.26.0", "babel-types": "^6.26.0" } }, "babel-plugin-transform-es2015-modules-systemjs": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", "requires": { "babel-helper-hoist-variables": "^6.24.1", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1" } }, "babel-plugin-transform-es2015-modules-umd": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", "requires": { "babel-plugin-transform-es2015-modules-amd": "^6.24.1", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1" } }, "babel-plugin-transform-es2015-object-super": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", "requires": { "babel-helper-replace-supers": "^6.24.1", "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-parameters": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", "requires": { "babel-helper-call-delegate": "^6.24.1", "babel-helper-get-function-arity": "^6.24.1", "babel-runtime": "^6.22.0", "babel-template": "^6.24.1", "babel-traverse": "^6.24.1", "babel-types": "^6.24.1" } }, "babel-plugin-transform-es2015-shorthand-properties": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", "requires": { "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "babel-plugin-transform-es2015-spread": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", "requires": { "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-sticky-regex": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", "requires": { "babel-helper-regex": "^6.24.1", "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "babel-plugin-transform-es2015-template-literals": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", "requires": { "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-typeof-symbol": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", "requires": { "babel-runtime": "^6.22.0" } }, "babel-plugin-transform-es2015-unicode-regex": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", "requires": { "babel-helper-regex": "^6.24.1", "babel-runtime": "^6.22.0", "regexpu-core": "^2.0.0" } }, "babel-plugin-transform-regenerator": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", "requires": { "regenerator-transform": "^0.10.0" } }, "babel-plugin-transform-strict-mode": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", "requires": { "babel-runtime": "^6.22.0", "babel-types": "^6.24.1" } }, "babel-preset-es2015": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", "requires": { "babel-plugin-check-es2015-constants": "^6.22.0", "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", "babel-plugin-transform-es2015-block-scoping": "^6.24.1", "babel-plugin-transform-es2015-classes": "^6.24.1", "babel-plugin-transform-es2015-computed-properties": "^6.24.1", "babel-plugin-transform-es2015-destructuring": "^6.22.0", "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1", "babel-plugin-transform-es2015-for-of": "^6.22.0", "babel-plugin-transform-es2015-function-name": "^6.24.1", "babel-plugin-transform-es2015-literals": "^6.22.0", "babel-plugin-transform-es2015-modules-amd": "^6.24.1", "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", "babel-plugin-transform-es2015-modules-umd": "^6.24.1", "babel-plugin-transform-es2015-object-super": "^6.24.1", "babel-plugin-transform-es2015-parameters": "^6.24.1", "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1", "babel-plugin-transform-es2015-spread": "^6.22.0", "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", "babel-plugin-transform-es2015-template-literals": "^6.22.0", "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0", "babel-plugin-transform-es2015-unicode-regex": "^6.24.1", "babel-plugin-transform-regenerator": "^6.24.1" } }, "babel-register": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", "requires": { "babel-core": "^6.26.0", "babel-runtime": "^6.26.0", "core-js": "^2.5.0", "home-or-tmp": "^2.0.0", "lodash": "^4.17.4", "mkdirp": "^0.5.1", "source-map-support": "^0.4.15" }, "dependencies": { "source-map-support": { "version": "0.4.18", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", "requires": { "source-map": "^0.5.6" } } } }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "requires": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" } }, "babel-template": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", "requires": { "babel-runtime": "^6.26.0", "babel-traverse": "^6.26.0", "babel-types": "^6.26.0", "babylon": "^6.18.0", "lodash": "^4.17.4" } }, "babel-traverse": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", "requires": { "babel-code-frame": "^6.26.0", "babel-messages": "^6.23.0", "babel-runtime": "^6.26.0", "babel-types": "^6.26.0", "babylon": "^6.18.0", "debug": "^2.6.8", "globals": "^9.18.0", "invariant": "^2.2.2", "lodash": "^4.17.4" } }, "babel-types": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "requires": { "babel-runtime": "^6.26.0", "esutils": "^2.0.2", "lodash": "^4.17.4", "to-fast-properties": "^1.0.3" } }, "babylon": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", "requires": { "cache-base": "^1.0.1", "class-utils": "^0.3.5", "component-emitter": "^1.2.1", "define-property": "^1.0.0", "isobject": "^3.0.1", "mixin-deep": "^1.2.0", "pascalcase": "^0.1.1" }, "dependencies": { "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "requires": { "is-descriptor": "^1.0.0" } }, "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "requires": { "kind-of": "^6.0.0" } }, "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "requires": { "kind-of": "^6.0.0" } }, "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } } } }, "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" }, "binary-extensions": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" }, "bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "optional": true, "requires": { "file-uri-to-path": "1.0.0" } }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "braces": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "requires": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", "extend-shallow": "^2.0.1", "fill-range": "^4.0.0", "isobject": "^3.0.1", "repeat-element": "^1.1.2", "snapdragon": "^0.8.1", "snapdragon-node": "^2.0.1", "split-string": "^3.0.2", "to-regex": "^3.0.1" }, "dependencies": { "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { "is-extendable": "^0.1.0" } } } }, "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "requires": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.3", "inherits": "^2.0.1", "safe-buffer": "^5.0.1" } }, "browserify-cipher": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", "requires": { "browserify-aes": "^1.0.4", "browserify-des": "^1.0.0", "evp_bytestokey": "^1.0.0" } }, "browserify-des": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", "requires": { "cipher-base": "^1.0.1", "des.js": "^1.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "browserify-rsa": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "requires": { "bn.js": "^4.1.0", "randombytes": "^2.0.1" } }, "browserify-sign": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", "requires": { "bn.js": "^4.1.1", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "create-hmac": "^1.1.2", "elliptic": "^6.0.0", "inherits": "^2.0.1", "parse-asn1": "^5.0.0" } }, "browserify-zlib": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", "requires": { "pako": "~1.0.5" } }, "browserslist": { "version": "4.14.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.1.tgz", "integrity": "sha512-zyBTIHydW37pnb63c7fHFXUG6EcqWOqoMdDx6cdyaDFriZ20EoVxcE95S54N+heRqY8m8IUgB5zYta/gCwSaaA==", "dev": true, "requires": { "caniuse-lite": "^1.0.30001124", "electron-to-chromium": "^1.3.562", "escalade": "^3.0.2", "node-releases": "^1.1.60" } }, "buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, "builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" }, "cacache": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", "requires": { "bluebird": "^3.5.5", "chownr": "^1.1.1", "figgy-pudding": "^3.5.1", "glob": "^7.1.4", "graceful-fs": "^4.1.15", "infer-owner": "^1.0.3", "lru-cache": "^5.1.1", "mississippi": "^3.0.0", "mkdirp": "^0.5.1", "move-concurrently": "^1.0.1", "promise-inflight": "^1.0.1", "rimraf": "^2.6.3", "ssri": "^6.0.1", "unique-filename": "^1.1.1", "y18n": "^4.0.0" } }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", "requires": { "collection-visit": "^1.0.0", "component-emitter": "^1.2.1", "get-value": "^2.0.6", "has-value": "^1.0.0", "isobject": "^3.0.1", "set-value": "^2.0.0", "to-object-path": "^0.3.0", "union-value": "^1.0.0", "unset-value": "^1.0.0" } }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "caniuse-lite": { - "version": "1.0.30001125", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001125.tgz", - "integrity": "sha512-9f+r7BW8Qli917mU3j0fUaTweT3f3vnX/Lcs+1C73V+RADmFme+Ih0Br8vONQi3X0lseOe6ZHfsZLCA8MSjxUA==", + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", "dev": true }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "requires": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" } }, "chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "requires": { "anymatch": "^2.0.0", "async-each": "^1.0.1", "braces": "^2.3.2", "fsevents": "^1.2.7", "glob-parent": "^3.1.0", "inherits": "^2.0.3", "is-binary-path": "^1.0.0", "is-glob": "^4.0.0", "normalize-path": "^3.0.0", "path-is-absolute": "^1.0.0", "readdirp": "^2.2.1", "upath": "^1.1.1" } }, "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "chrome-trace-event": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", "requires": { "tslib": "^1.9.0" } }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" } }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", "requires": { "arr-union": "^3.1.0", "define-property": "^0.2.5", "isobject": "^3.0.0", "static-extend": "^0.1.1" }, "dependencies": { "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "requires": { "is-descriptor": "^0.1.0" } } } }, "cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "requires": { "string-width": "^3.1.0", "strip-ansi": "^5.2.0", "wrap-ansi": "^5.1.0" }, "dependencies": { "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "requires": { "ansi-regex": "^4.1.0" } } } }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", "requires": { "map-visit": "^1.0.0", "object-visit": "^1.0.0" } }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "requires": { "color-name": "1.1.3" } }, "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" }, "convert-source-map": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", "requires": { "safe-buffer": "~5.1.1" } }, "copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", "requires": { "aproba": "^1.1.1", "fs-write-stream-atomic": "^1.0.8", "iferr": "^0.1.5", "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.0" } }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, "core-js": { "version": "2.6.11", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" }, "core-js-compat": { "version": "3.6.5", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz", "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==", "dev": true, "requires": { "browserslist": "^4.8.5", "semver": "7.0.0" }, "dependencies": { "semver": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", "dev": true } } }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", "requires": { "bn.js": "^4.1.0", "elliptic": "^6.0.0" } }, "create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "requires": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", "md5.js": "^1.3.4", "ripemd160": "^2.0.1", "sha.js": "^2.4.0" } }, "create-hmac": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "requires": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", "inherits": "^2.0.1", "ripemd160": "^2.0.0", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "crel": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/crel/-/crel-4.2.0.tgz", "integrity": "sha512-ZBVhTmtapdjEn8rTcTR/ePE0JSNfHzZfDkD3eAnsQEBmkQW9r14mzq6tK5Hkw6r26l7XxlU2Ox1nfHsmGIj0zQ==" }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "requires": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", "requires": { "browserify-cipher": "^1.0.0", "browserify-sign": "^4.0.0", "create-ecdh": "^4.0.0", "create-hash": "^1.1.0", "create-hmac": "^1.1.0", "diffie-hellman": "^5.0.0", "inherits": "^2.0.1", "pbkdf2": "^3.0.3", "public-encrypt": "^4.0.0", "randombytes": "^2.0.0", "randomfill": "^1.0.3" } }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=" }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "requires": { "ms": "2.0.0" } }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "requires": { "object-keys": "^1.0.12" } }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", "requires": { "is-descriptor": "^1.0.2", "isobject": "^3.0.1" }, "dependencies": { "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "requires": { "kind-of": "^6.0.0" } }, "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "requires": { "kind-of": "^6.0.0" } }, "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } } } }, "des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", "requires": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=" }, "detect-indent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", "requires": { "repeating": "^2.0.0" } }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "requires": { "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" } }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "requires": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "electron-to-chromium": { "version": "1.3.564", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.564.tgz", "integrity": "sha512-fNaYN3EtKQWLQsrKXui8mzcryJXuA0LbCLoizeX6oayG2emBaS5MauKjCPAvc29NEY4FpLHIUWiP+Y0Bfrs5dg==", "dev": true }, "elliptic": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", "requires": { "bn.js": "^4.4.0", "brorand": "^1.0.1", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.0" } }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "requires": { "once": "^1.4.0" } }, "enhanced-resolve": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", "requires": { "graceful-fs": "^4.1.2", "memory-fs": "^0.5.0", "tapable": "^1.0.0" }, "dependencies": { "memory-fs": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", "requires": { "errno": "^0.1.3", "readable-stream": "^2.0.1" } } } }, "entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" }, "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", "requires": { "prr": "~1.0.1" } }, "escalade": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz", "integrity": "sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==", "dev": true }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", "requires": { "esrecurse": "^4.1.0", "estraverse": "^4.1.1" } }, "esrecurse": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", "requires": { "estraverse": "^4.1.0" } }, "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" }, "events": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==" }, "evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", "requires": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" } }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "requires": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", "requires": { "debug": "^2.3.3", "define-property": "^0.2.5", "extend-shallow": "^2.0.1", "posix-character-classes": "^0.1.0", "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.1" }, "dependencies": { "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "requires": { "is-descriptor": "^0.1.0" } }, "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { "is-extendable": "^0.1.0" } } } }, "expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", "requires": { "homedir-polyfill": "^1.0.1" } }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "requires": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" }, "dependencies": { "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "requires": { "is-plain-object": "^2.0.4" } } } }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", "requires": { "array-unique": "^0.3.2", "define-property": "^1.0.0", "expand-brackets": "^2.1.4", "extend-shallow": "^2.0.1", "fragment-cache": "^0.2.1", "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.1" }, "dependencies": { "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "requires": { "is-descriptor": "^1.0.0" } }, "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { "is-extendable": "^0.1.0" } }, "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "requires": { "kind-of": "^6.0.0" } }, "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "requires": { "kind-of": "^6.0.0" } }, "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } } } }, "fast-deep-equal": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "figgy-pudding": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==" }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "optional": true }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", "repeat-string": "^1.6.1", "to-regex-range": "^2.1.0" }, "dependencies": { "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { "is-extendable": "^0.1.0" } } } }, "find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", "requires": { "commondir": "^1.0.1", "make-dir": "^2.0.0", "pkg-dir": "^3.0.0" } }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "requires": { "locate-path": "^3.0.0" } }, "findup-sync": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", "requires": { "detect-file": "^1.0.0", "is-glob": "^4.0.0", "micromatch": "^3.0.4", "resolve-dir": "^1.0.1" } }, "flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", "requires": { "inherits": "^2.0.3", "readable-stream": "^2.3.6" } }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", "requires": { "map-cache": "^0.2.2" } }, "from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", "requires": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" } }, "fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", "requires": { "graceful-fs": "^4.1.2", "iferr": "^0.1.5", "imurmurhash": "^0.1.4", "readable-stream": "1 || 2" } }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.2.11", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.11.tgz", "integrity": "sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==", "optional": true, "requires": { "bindings": "^1.5.0", "nan": "^2.12.1", "node-pre-gyp": "*" }, "dependencies": { "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "bundled": true, "optional": true }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "bundled": true, "optional": true }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "bundled": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "bundled": true, "optional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" } }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "bundled": true, "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "bundled": true, "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "chownr": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", "bundled": true, "optional": true }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "bundled": true, "optional": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "bundled": true, "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "bundled": true, "optional": true }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "bundled": true, "optional": true }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "bundled": true, "optional": true, "requires": { "ms": "^2.1.1" } }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "bundled": true, "optional": true }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "bundled": true, "optional": true }, "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "bundled": true, "optional": true }, "fs-minipass": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", "bundled": true, "optional": true, "requires": { "minipass": "^2.6.0" } }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "bundled": true, "optional": true }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "bundled": true, "optional": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", "has-unicode": "^2.0.0", "object-assign": "^4.1.0", "signal-exit": "^3.0.0", "string-width": "^1.0.1", "strip-ansi": "^3.0.1", "wide-align": "^1.1.0" } }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "bundled": true, "optional": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "bundled": true, "optional": true }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "bundled": true, "optional": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", "bundled": true, "optional": true, "requires": { "minimatch": "^3.0.4" } }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "bundled": true, "optional": true, "requires": { "once": "^1.3.0", "wrappy": "1" } }, "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "bundled": true, "optional": true }, "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "bundled": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "bundled": true, "optional": true, "requires": { "number-is-nan": "^1.0.0" } }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "bundled": true, "optional": true }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "bundled": true, "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "bundled": true, "optional": true }, "minipass": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", "bundled": true, "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "minizlib": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", "bundled": true, "optional": true, "requires": { "minipass": "^2.9.0" } }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "bundled": true, "optional": true, "requires": { "minimist": "0.0.8" } }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "bundled": true, "optional": true }, "needle": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", "bundled": true, "optional": true, "requires": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", "sax": "^1.2.4" } }, "node-pre-gyp": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz", "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==", "bundled": true, "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", "needle": "^2.2.1", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4.4.2" } }, "nopt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "bundled": true, "optional": true, "requires": { "abbrev": "1", "osenv": "^0.1.4" } }, "npm-bundled": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", "bundled": true, "optional": true, "requires": { "npm-normalize-package-bin": "^1.0.1" } }, "npm-normalize-package-bin": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", "bundled": true, "optional": true }, "npm-packlist": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.7.tgz", "integrity": "sha512-vAj7dIkp5NhieaGZxBJB8fF4R0078rqsmhJcAfXZ6O7JJhjhPK96n5Ry1oZcfLXgfun0GWTZPOxaEyqv8GBykQ==", "bundled": true, "optional": true, "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1" } }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "bundled": true, "optional": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", "gauge": "~2.7.3", "set-blocking": "~2.0.0" } }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "bundled": true, "optional": true }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "bundled": true, "optional": true }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "bundled": true, "optional": true, "requires": { "wrappy": "1" } }, "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "bundled": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "bundled": true, "optional": true }, "osenv": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "bundled": true, "optional": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" } }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "bundled": true, "optional": true }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "bundled": true, "optional": true }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "bundled": true, "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "dependencies": { "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "bundled": true, "optional": true } } }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "bundled": true, "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "bundled": true, "optional": true, "requires": { "glob": "^7.1.3" } }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "bundled": true, "optional": true }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "bundled": true, "optional": true }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "bundled": true, "optional": true }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "bundled": true, "optional": true }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "bundled": true, "optional": true }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "bundled": true, "optional": true }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "bundled": true, "optional": true, "requires": { "safe-buffer": "~5.1.0" } }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "bundled": true, "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "bundled": true, "optional": true, "requires": { "ansi-regex": "^2.0.0" } }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "bundled": true, "optional": true }, "tar": { "version": "4.4.13", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", "bundled": true, "optional": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", "minipass": "^2.8.6", "minizlib": "^1.2.1", "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", "yallist": "^3.0.3" } }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "bundled": true, "optional": true }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "bundled": true, "optional": true, "requires": { "string-width": "^1.0.2 || 2" } }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "bundled": true, "optional": true }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "bundled": true, "optional": true } } }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, "gensync": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", "dev": true }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "requires": { "pump": "^3.0.0" } }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "requires": { "is-glob": "^3.1.0", "path-dirname": "^1.0.0" }, "dependencies": { "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "requires": { "is-extglob": "^2.1.0" } } } }, "global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", "requires": { "global-prefix": "^3.0.0" }, "dependencies": { "global-prefix": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", "requires": { "ini": "^1.3.5", "kind-of": "^6.0.2", "which": "^1.3.1" } } } }, "global-prefix": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", "requires": { "expand-tilde": "^2.0.2", "homedir-polyfill": "^1.0.1", "ini": "^1.3.4", "is-windows": "^1.0.1", "which": "^1.2.14" } }, "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" }, "graceful-fs": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "requires": { "ansi-regex": "^2.0.0" } }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", "requires": { "get-value": "^2.0.6", "has-values": "^1.0.0", "isobject": "^3.0.0" } }, "has-values": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", "requires": { "is-number": "^3.0.0", "kind-of": "^4.0.0" }, "dependencies": { "kind-of": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", "requires": { "is-buffer": "^1.1.5" } } } }, "hash-base": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" } }, "hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" } }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", "requires": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.1" } }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", "requires": { "parse-passwd": "^1.0.0" } }, "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "iferr": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" }, "import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", "requires": { "pkg-dir": "^3.0.0", "resolve-cwd": "^2.0.0" } }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, "infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { "once": "^1.3.0", "wrappy": "1" } }, "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "interpret": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==" }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "requires": { "loose-envify": "^1.0.0" } }, "invert-kv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "requires": { "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { "is-buffer": "^1.1.5" } } } }, "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", "requires": { "binary-extensions": "^1.0.0" } }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "requires": { "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { "is-buffer": "^1.1.5" } } } }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "requires": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", "kind-of": "^5.0.0" }, "dependencies": { "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" } } }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-finite": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==" }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", "requires": { "is-extglob": "^2.1.1" } }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "requires": { "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { "is-buffer": "^1.1.5" } } } }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "requires": { "isobject": "^3.0.1" } }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" }, "is-wsl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" }, "jsesc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json5": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "requires": { "minimist": "^1.2.0" } }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, "lcid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", "requires": { "invert-kv": "^2.0.0" } }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true }, "levenary": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz", "integrity": "sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==", "dev": true, "requires": { "leven": "^3.1.0" } }, "linkify-it": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz", "integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==", "requires": { "uc.micro": "^1.0.1" } }, "loader-runner": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==" }, "loader-utils": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", "requires": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^1.0.1" } }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "requires": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "requires": { "yallist": "^3.0.2" } }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "requires": { "pify": "^4.0.1", "semver": "^5.6.0" } }, "mamacro": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==" }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", "requires": { "p-defer": "^1.0.0" } }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", "requires": { "object-visit": "^1.0.0" } }, "markdown-it": { "version": "12.0.4", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.4.tgz", "integrity": "sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q==", "requires": { "argparse": "^2.0.1", "entities": "~2.1.0", "linkify-it": "^3.0.1", "mdurl": "^1.0.1", "uc.micro": "^1.0.5" } }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1", "safe-buffer": "^5.1.2" } }, "mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" }, "mem": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", "requires": { "map-age-cleaner": "^0.1.1", "mimic-fn": "^2.0.0", "p-is-promise": "^2.0.0" } }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", "requires": { "errno": "^0.1.3", "readable-stream": "^2.0.1" } }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", "braces": "^2.3.1", "define-property": "^2.0.2", "extend-shallow": "^3.0.2", "extglob": "^2.0.4", "fragment-cache": "^0.2.1", "kind-of": "^6.0.2", "nanomatch": "^1.2.9", "object.pick": "^1.3.0", "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.2" } }, "miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", "requires": { "bn.js": "^4.0.0", "brorand": "^1.0.1" } }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", "requires": { "concat-stream": "^1.5.0", "duplexify": "^3.4.2", "end-of-stream": "^1.1.0", "flush-write-stream": "^1.0.0", "from2": "^2.1.0", "parallel-transform": "^1.1.0", "pump": "^3.0.0", "pumpify": "^1.3.3", "stream-each": "^1.1.0", "through2": "^2.0.0" } }, "mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "requires": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" }, "dependencies": { "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "requires": { "is-plain-object": "^2.0.4" } } } }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" }, "dependencies": { "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", "requires": { "aproba": "^1.1.1", "copy-concurrently": "^1.0.0", "fs-write-stream-atomic": "^1.0.8", "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.3" } }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", "optional": true }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", "define-property": "^2.0.2", "extend-shallow": "^3.0.2", "fragment-cache": "^0.2.1", "is-windows": "^1.0.2", "kind-of": "^6.0.2", "object.pick": "^1.3.0", "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.1" } }, "neo-async": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "node-libs-browser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", "requires": { "assert": "^1.1.1", "browserify-zlib": "^0.2.0", "buffer": "^4.3.0", "console-browserify": "^1.1.0", "constants-browserify": "^1.0.0", "crypto-browserify": "^3.11.0", "domain-browser": "^1.1.1", "events": "^3.0.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "0.0.1", "process": "^0.11.10", "punycode": "^1.2.4", "querystring-es3": "^0.2.0", "readable-stream": "^2.3.3", "stream-browserify": "^2.0.1", "stream-http": "^2.7.2", "string_decoder": "^1.0.0", "timers-browserify": "^2.0.4", "tty-browserify": "0.0.0", "url": "^0.11.0", "util": "^0.11.0", "vm-browserify": "^1.0.1" }, "dependencies": { "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" } } }, "node-releases": { "version": "1.1.61", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.61.tgz", "integrity": "sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g==", "dev": true }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "requires": { "path-key": "^2.0.0" } }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", "requires": { "copy-descriptor": "^0.1.0", "define-property": "^0.2.5", "kind-of": "^3.0.3" }, "dependencies": { "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "requires": { "is-descriptor": "^0.1.0" } }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { "is-buffer": "^1.1.5" } } } }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", "requires": { "isobject": "^3.0.0" } }, "object.assign": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", "has-symbols": "^1.0.0", "object-keys": "^1.0.11" } }, "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", "requires": { "isobject": "^3.0.1" } }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" } }, "orderedmap": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.1.tgz", "integrity": "sha512-3Ux8um0zXbVacKUkcytc0u3HgC0b0bBLT+I60r2J/En72cI0nZffqrA7Xtf2Hqs27j1g82llR5Mhbd0Z1XW4AQ==" }, "os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" }, "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-locale": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", "requires": { "execa": "^1.0.0", "lcid": "^2.0.0", "mem": "^4.0.0" } }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, "p-is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" }, "p-limit": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", "requires": { "p-try": "^2.0.0" } }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "requires": { "p-limit": "^2.0.0" } }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "parallel-transform": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", "requires": { "cyclist": "^1.0.1", "inherits": "^2.0.3", "readable-stream": "^2.1.5" } }, "parse-asn1": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", "requires": { "asn1.js": "^4.0.0", "browserify-aes": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", "pbkdf2": "^3.0.3", "safe-buffer": "^5.1.1" } }, "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" }, "path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==" }, "path-dirname": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, "pbkdf2": { "version": "3.0.17", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", "requires": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", "ripemd160": "^2.0.1", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" }, "pkg-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", "requires": { "find-up": "^3.0.0" } }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, "prettier": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", "dev": true }, "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" }, "prosemirror-commands": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.1.3.tgz", "integrity": "sha512-YVbKwTR4likoyhuwIUC9egbzHvnFrFUNbiesB0DB/HZ8hBcopQ42Tb4KGlYrS3n+pNDTFObN73CLFY6mYLN2IQ==", "requires": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "prosemirror-dropcursor": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.3.2.tgz", "integrity": "sha512-4c94OUGyobGnwcQI70OXyMhE/9T4aTgjU+CHxkd5c7D+jH/J0mKM/lk+jneFVKt7+E4/M0D9HzRPifu8U28Thw==", "requires": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "prosemirror-example-setup": { "version": "git+ssh://git@github.com/writefreely/prosemirror-example-setup.git#aaea7d62ebc2fa769139faf8a097449441c8237c", "from": "prosemirror-example-setup@github:writefreely/prosemirror-example-setup", "requires": { "prosemirror-commands": "^1.0.0", "prosemirror-dropcursor": "^1.0.0", "prosemirror-gapcursor": "^1.0.0", "prosemirror-history": "^1.0.0", "prosemirror-inputrules": "^1.0.0", "prosemirror-keymap": "^1.0.0", "prosemirror-menu": "^1.0.0", "prosemirror-schema-list": "^1.0.0", "prosemirror-state": "^1.0.0" } }, "prosemirror-gapcursor": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.1.3.tgz", "integrity": "sha512-/lgWvt2AdHjsM6oEsF65z0lhdQJGl6sQSfXSOX8/xjpd8ycfOolhgKZd4TPYpikwnh85JF4l5eIyiFZsl/RQQA==", "requires": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "prosemirror-history": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.1.3.tgz", "integrity": "sha512-zGDotijea+vnfnyyUGyiy1wfOQhf0B/b6zYcCouBV8yo6JmrE9X23M5q7Nf/nATywEZbgRLG70R4DmfSTC+gfg==", "requires": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "rope-sequence": "^1.3.0" } }, "prosemirror-inputrules": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.1.2.tgz", "integrity": "sha512-Ja5Z3BWestlHYGvtSGqyvxMeB8QEuBjlHM8YnKtLGUXMDp965qdDV4goV8lJb17kIWHk7e7JNj6Catuoa3302g==", "requires": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "prosemirror-keymap": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.1.4.tgz", "integrity": "sha512-Al8cVUOnDFL4gcI5IDlG6xbZ0aOD/i3B17VT+1JbHWDguCgt/lBHVTHUBcKvvbSg6+q/W4Nj1Fu6bwZSca3xjg==", "requires": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "prosemirror-markdown": { "version": "git+ssh://git@github.com/writefreely/prosemirror-markdown.git#809d0a444cf4d366d7c0c350d881df1b55d172f2", "from": "prosemirror-markdown@github:writefreely/prosemirror-markdown", "requires": { "markdown-it": "^10.0.0", "prosemirror-model": "^1.0.0" }, "dependencies": { "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "requires": { "sprintf-js": "~1.0.2" } }, "entities": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==" }, "linkify-it": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", "requires": { "uc.micro": "^1.0.1" } }, "markdown-it": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", "requires": { "argparse": "^1.0.7", "entities": "~2.0.0", "linkify-it": "^2.0.0", "mdurl": "^1.0.1", "uc.micro": "^1.0.5" } } } }, "prosemirror-menu": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.1.3.tgz", "integrity": "sha512-G+XiTdiVi1uGeiBR1OqtiVP3p0XyCKec4DYLk75G7J2R/N8OThHt/1StJYDlXPNzqYsQFZfQyPp/UwOL8hD6AA==", "requires": { "crel": "^4.2.0", "prosemirror-commands": "^1.0.0", "prosemirror-history": "^1.0.0", "prosemirror-state": "^1.0.0" } }, "prosemirror-model": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.9.1.tgz", "integrity": "sha512-Qblh8pm1c7Ll64sYLauwwzjimo/tFg1zW3Q3IWhKRhvfOEgRKqa6dC5pRrAa+XHOIjBFEYrqbi52J5bqA2dV8Q==", "requires": { "orderedmap": "^1.1.0" } }, "prosemirror-schema-list": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.1.2.tgz", "integrity": "sha512-dgM9PwtM4twa5WsgSYMB+J8bwjnR43DAD3L9MsR9rKm/nZR5Y85xcjB7gusVMSsbQ2NomMZF03RE6No6mTnclQ==", "requires": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "prosemirror-state": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.3.2.tgz", "integrity": "sha512-t/JqE3aR0SV9QrzFVkAXsQwsgrQBNs/BDbcFH20RssW0xauqNNdjTXxy/J/kM7F+0zYi6+BRmz7cMMQQFU3mwQ==", "requires": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "prosemirror-transform": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.2.4.tgz", "integrity": "sha512-0A668uf0EN89L9O9brE05kHcqp7FHmT5YN7Tom58Kj926QqOBs7iNRHDLWxrSaQB5MNZtzDOD9T3EyJ88YDcBg==", "requires": { "prosemirror-model": "^1.0.0" } }, "prosemirror-view": { "version": "1.14.2", "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.14.2.tgz", "integrity": "sha512-9yPVH6OLyaEraHjWHbSk2DB0R/1TsEE6AA1LI+vmCypXXA+zTzNrktUFzBhSJHehXDoEJcQfnl1Wdp5GPSh2+g==", "requires": { "prosemirror-model": "^1.1.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" }, "public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", "requires": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" } }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "pumpify": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "requires": { "duplexify": "^3.6.0", "inherits": "^2.0.3", "pump": "^2.0.0" }, "dependencies": { "pump": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } } } }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, "querystring-es3": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "requires": { "safe-buffer": "^5.1.0" } }, "randomfill": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", "requires": { "randombytes": "^2.0.5", "safe-buffer": "^5.1.0" } }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", "requires": { "graceful-fs": "^4.1.11", "micromatch": "^3.1.10", "readable-stream": "^2.0.2" } }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" }, "regenerate-unicode-properties": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", "dev": true, "requires": { "regenerate": "^1.4.0" } }, "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, "regenerator-transform": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", "requires": { "babel-runtime": "^6.18.0", "babel-types": "^6.19.0", "private": "^0.1.6" } }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", "requires": { "extend-shallow": "^3.0.2", "safe-regex": "^1.1.0" } }, "regexpu-core": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", "requires": { "regenerate": "^1.2.1", "regjsgen": "^0.2.0", "regjsparser": "^0.1.4" } }, "regjsgen": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" }, "regjsparser": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", "requires": { "jsesc": "~0.5.0" }, "dependencies": { "jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" } } }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, "repeat-element": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" }, "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, "repeating": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "requires": { "is-finite": "^1.0.0" } }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "resolve": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", "dev": true, "requires": { "path-parse": "^1.0.6" } }, "resolve-cwd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", "requires": { "resolve-from": "^3.0.0" } }, "resolve-dir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", "requires": { "expand-tilde": "^2.0.0", "global-modules": "^1.0.0" }, "dependencies": { "global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", "requires": { "global-prefix": "^1.0.1", "is-windows": "^1.0.1", "resolve-dir": "^1.0.0" } } } }, "resolve-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { "glob": "^7.1.3" } }, "ripemd160": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, "rope-sequence": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.2.tgz", "integrity": "sha512-ku6MFrwEVSVmXLvy3dYph3LAMNS0890K7fabn+0YIRQ2T96T9F4gkFf0vf0WW0JUraNWwGRtInEpH7yO4tbQZg==" }, "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", "requires": { "aproba": "^1.1.1" } }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "requires": { "ret": "~0.1.10" } }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", "requires": { "ajv": "^6.1.0", "ajv-errors": "^1.0.0", "ajv-keywords": "^3.1.0" } }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "serialize-javascript": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==" }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "requires": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", "is-plain-object": "^2.0.3", "split-string": "^3.0.1" }, "dependencies": { "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { "is-extendable": "^0.1.0" } } } }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "sha.js": { "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" } }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "requires": { "shebang-regex": "^1.0.0" } }, "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", "requires": { "base": "^0.11.1", "debug": "^2.2.0", "define-property": "^0.2.5", "extend-shallow": "^2.0.1", "map-cache": "^0.2.2", "source-map": "^0.5.6", "source-map-resolve": "^0.5.0", "use": "^3.1.0" }, "dependencies": { "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "requires": { "is-descriptor": "^0.1.0" } }, "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { "is-extendable": "^0.1.0" } } } }, "snapdragon-node": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", "requires": { "define-property": "^1.0.0", "isobject": "^3.0.0", "snapdragon-util": "^3.0.1" }, "dependencies": { "define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "requires": { "is-descriptor": "^1.0.0" } }, "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "requires": { "kind-of": "^6.0.0" } }, "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "requires": { "kind-of": "^6.0.0" } }, "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } } } }, "snapdragon-util": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", "requires": { "kind-of": "^3.2.0" }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { "is-buffer": "^1.1.5" } } } }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", "requires": { "atob": "^2.1.2", "decode-uri-component": "^0.2.0", "resolve-url": "^0.2.1", "source-map-url": "^0.4.0", "urix": "^0.1.0" } }, "source-map-support": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" }, "dependencies": { "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", "requires": { "extend-shallow": "^3.0.0" } }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "ssri": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", "requires": { "figgy-pudding": "^3.5.1" } }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", "requires": { "define-property": "^0.2.5", "object-copy": "^0.1.0" }, "dependencies": { "define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "requires": { "is-descriptor": "^0.1.0" } } } }, "stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", "requires": { "inherits": "~2.0.1", "readable-stream": "^2.0.2" } }, "stream-each": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", "requires": { "end-of-stream": "^1.1.0", "stream-shift": "^1.0.0" } }, "stream-http": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", "requires": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.1", "readable-stream": "^2.3.6", "to-arraybuffer": "^1.0.0", "xtend": "^4.0.0" } }, "stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" } }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" }, "dependencies": { "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "requires": { "ansi-regex": "^4.1.0" } } } }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" } }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" }, "tapable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" }, "terser": { "version": "4.6.6", "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.6.tgz", "integrity": "sha512-4lYPyeNmstjIIESr/ysHg2vUPRGf2tzF9z2yYwnowXVuVzLEamPN1Gfrz7f8I9uEPuHcbFlW4PLIAsJoxXyJ1g==", "requires": { "commander": "^2.20.0", "source-map": "~0.6.1", "source-map-support": "~0.5.12" }, "dependencies": { "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, "terser-webpack-plugin": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", "serialize-javascript": "^2.1.2", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", "worker-farm": "^1.7.0" }, "dependencies": { "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "requires": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "timers-browserify": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", "requires": { "setimmediate": "^1.0.4" } }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" }, "to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", "requires": { "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { "is-buffer": "^1.1.5" } } } }, "to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", "requires": { "define-property": "^2.0.2", "extend-shallow": "^3.0.2", "regex-not": "^1.0.2", "safe-regex": "^1.1.0" } }, "to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "requires": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" } }, "trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" }, "tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", "dev": true }, "unicode-match-property-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", "dev": true, "requires": { "unicode-canonical-property-names-ecmascript": "^1.0.4", "unicode-property-aliases-ecmascript": "^1.0.4" } }, "unicode-match-property-value-ecmascript": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", "dev": true }, "unicode-property-aliases-ecmascript": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", "dev": true }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", "set-value": "^2.0.1" } }, "unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", "requires": { "unique-slug": "^2.0.0" } }, "unique-slug": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", "requires": { "imurmurhash": "^0.1.4" } }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", "requires": { "has-value": "^0.3.1", "isobject": "^3.0.0" }, "dependencies": { "has-value": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", "requires": { "get-value": "^2.0.3", "has-values": "^0.1.4", "isobject": "^2.0.0" }, "dependencies": { "isobject": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", "requires": { "isarray": "1.0.0" } } } }, "has-values": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" } } }, "upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "requires": { "punycode": "^2.1.0" } }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", "requires": { "punycode": "1.3.2", "querystring": "0.2.0" }, "dependencies": { "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" } } }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "requires": { "inherits": "2.0.3" }, "dependencies": { "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" } } }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "v8-compile-cache": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==" }, "vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, "w3c-keyname": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz", "integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==" }, "watchpack": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", "requires": { "chokidar": "^2.0.2", "graceful-fs": "^4.1.2", "neo-async": "^2.5.0" } }, "webpack": { "version": "4.42.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.0.tgz", "integrity": "sha512-EzJRHvwQyBiYrYqhyjW9AqM90dE4+s1/XtCfn7uWg6cS72zH+2VPFAlsnW0+W0cDi0XRjNKUMoJtpSi50+Ph6w==", "requires": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-module-context": "1.8.5", "@webassemblyjs/wasm-edit": "1.8.5", "@webassemblyjs/wasm-parser": "1.8.5", "acorn": "^6.2.1", "ajv": "^6.10.2", "ajv-keywords": "^3.4.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^4.1.0", "eslint-scope": "^4.0.3", "json-parse-better-errors": "^1.0.2", "loader-runner": "^2.4.0", "loader-utils": "^1.2.3", "memory-fs": "^0.4.1", "micromatch": "^3.1.10", "mkdirp": "^0.5.1", "neo-async": "^2.6.1", "node-libs-browser": "^2.2.1", "schema-utils": "^1.0.0", "tapable": "^1.1.3", "terser-webpack-plugin": "^1.4.3", "watchpack": "^1.6.0", "webpack-sources": "^1.4.1" } }, "webpack-cli": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.11.tgz", "integrity": "sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g==", "requires": { "chalk": "2.4.2", "cross-spawn": "6.0.5", "enhanced-resolve": "4.1.0", "findup-sync": "3.0.0", "global-modules": "2.0.0", "import-local": "2.0.0", "interpret": "1.2.0", "loader-utils": "1.2.3", "supports-color": "6.1.0", "v8-compile-cache": "2.0.3", "yargs": "13.2.4" }, "dependencies": { "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "requires": { "color-convert": "^1.9.0" } }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" }, "dependencies": { "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "requires": { "has-flag": "^3.0.0" } } } }, "emojis-list": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" }, "enhanced-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", "requires": { "graceful-fs": "^4.1.2", "memory-fs": "^0.4.0", "tapable": "^1.0.0" } }, "loader-utils": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", "requires": { "big.js": "^5.2.2", "emojis-list": "^2.0.0", "json5": "^1.0.1" } }, "supports-color": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", "requires": { "has-flag": "^3.0.0" } } } }, "webpack-sources": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", "requires": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" }, "dependencies": { "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "requires": { "isexe": "^2.0.0" } }, "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "worker-farm": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", "requires": { "errno": "~0.1.7" } }, "wrap-ansi": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "requires": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", "strip-ansi": "^5.0.0" }, "dependencies": { "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "requires": { "color-convert": "^1.9.0" } }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "requires": { "ansi-regex": "^4.1.0" } } } }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "yargs": { "version": "13.2.4", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", "requires": { "cliui": "^5.0.0", "find-up": "^3.0.0", "get-caller-file": "^2.0.1", "os-locale": "^3.1.0", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^13.1.0" } }, "yargs-parser": { "version": "13.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } } } } diff --git a/prose/prose.js b/prose/prose.js index 964688f..cb39c6d 100644 --- a/prose/prose.js +++ b/prose/prose.js @@ -1,121 +1,120 @@ // class MarkdownView { // constructor(target, content) { // this.textarea = target.appendChild(document.createElement("textarea")) // this.textarea.value = content // } // get content() { return this.textarea.value } // focus() { this.textarea.focus() } // destroy() { this.textarea.remove() } // } import { EditorView } from "prosemirror-view"; import { EditorState, TextSelection } from "prosemirror-state"; import { exampleSetup } from "prosemirror-example-setup"; import { keymap } from "prosemirror-keymap"; import { writeFreelyMarkdownParser } from "./markdownParser"; import { writeFreelyMarkdownSerializer } from "./markdownSerializer"; import { writeFreelySchema } from "./schema"; import { getMenu } from "./menu"; let $title = document.querySelector("#title"); let $content = document.querySelector("#content"); // Bugs: // 1. When there's just an empty line and a hard break is inserted with shift-enter then two enters are inserted // which do not show up in the markdown ( maybe bc. they are training enters ) class ProseMirrorView { constructor(target, content) { let typingTimer; let localDraft = localStorage.getItem(window.draftKey); if (localDraft != null) { content = localDraft; } if (content.indexOf("# ") === 0) { let eol = content.indexOf("\n"); let title = content.substring("# ".length, eol); content = content.substring(eol + "\n\n".length); $title.value = title; } const doc = writeFreelyMarkdownParser.parse(content) this.view = new EditorView(target, { state: EditorState.create({ doc, plugins: [ keymap({ "Mod-Enter": () => { document.getElementById("publish").click(); return true; }, "Mod-k": () => { const linkButton = document.querySelector( ".ProseMirror-icon[title='Add or remove link']" ); linkButton.dispatchEvent(new Event("mousedown")); return true; }, }), ...exampleSetup({ schema: writeFreelySchema, menuContent: getMenu(), }), ], }), dispatchTransaction(transaction) { let newState = this.state.apply(transaction); const newContent = writeFreelyMarkdownSerializer .serialize(newState.doc) // Replace all \\\ns ( not followed by a \n ) with \n .replace(/(\\\n)(\n{0,1})/g, (match, p1, p2) => p2 !== "\n" ? "\n" + p2 : match ); $content.value = newContent; let draft = ""; if ($title.value != null && $title.value !== "") { draft = "# " + $title.value + "\n\n"; } draft += newContent; clearTimeout(typingTimer); typingTimer = setTimeout(doneTyping, doneTypingInterval); this.updateState(newState); }, handleDOMEvents: { drop: (view, event) => { // If a file is dropped externally into the editor, do not insert anything. This will not trigger if an image has been inserted after upload and is dragged and dropped internally to change its position. if (event.dataTransfer.files.length > 0) { event.preventDefault(); } } }, }); // Editor is focused to the last position. This is a workaround for a bug: // 1. 1 type something in an existing entry // 2. reload - works fine, the draft is reloaded // 3. reload again - the draft is somehow removed from localStorage and the original content is loaded // When the editor is focused the content is re-saved to localStorage // This is also useful for editing, so it's not a bad thing even const lastPosition = this.view.state.doc.content.size; const selection = TextSelection.create(this.view.state.doc, lastPosition); this.view.dispatch(this.view.state.tr.setSelection(selection)); this.view.focus(); } get content() { return writeFreelyMarkdownSerializer.serialize(this.view.state.doc); } focus() { this.view.focus(); } destroy() { this.view.destroy(); } } let place = document.querySelector("#editor"); let view = new ProseMirrorView(place, $content.value); -window.editorView = view; diff --git a/prose/schema.js b/prose/schema.js index 91a2be9..eeebbd9 100644 --- a/prose/schema.js +++ b/prose/schema.js @@ -1,62 +1,19 @@ import { schema } from "prosemirror-markdown"; import { Schema } from "prosemirror-model"; export const writeFreelySchema = new Schema({ nodes: schema.spec.nodes .addToEnd("readmore", { inline: false, content: "", group: "block", draggable: true, toDOM: (node) => [ "div", { class: "editorreadmore" }, "Read more...", ], parseDOM: [{ tag: "div.editorreadmore" }], - }) - .addToEnd("emailsub", { - inline: false, - content: "", - group: "block", - draggable: true, - toDOM: () => [ - "div", { id: "emailsub", contenteditable: "false" }, - ["form", {}, - ["input", { type: "email", disabled: true, placeholder: "me@example.com" }], - ["input", { type: "submit", disabled: true, value: "Subscribe" }], - ], - ], - parseDOM: [{ tag: "div#emailsub" }], - }) - .addToEnd("comment", { - inline: true, - content: "", - group: "inline", - draggable: false, - toDOM: () => ["a", { class: "editor-comment" }, "Discuss..."], - parseDOM: [{ tag: "a.editor-comment" }], - }) - .addToEnd("html_block", { - attrs: { content: { default: "" } }, - content: "", - group: "block", - marks: "", - draggable: true, - toDOM: (node) => [ - "div", - { class: "editor-html-block", contenteditable: "false" }, - node.attrs.content, - ], - parseDOM: [{ tag: "div.editor-html-block", getAttrs: (dom) => ({ content: dom.textContent }) }], - }) - .addToEnd("html_inline", { - attrs: { content: { default: "" } }, - inline: true, - content: "", - group: "inline", - toDOM: (node) => ["code", { class: "editor-html-inline" }, node.attrs.content], - parseDOM: [{ tag: "code.editor-html-inline", getAttrs: (dom) => ({ content: dom.textContent }) }], }), marks: schema.spec.marks, }); diff --git a/read.go b/read.go index 9223377..5f5ff5c 100644 --- a/read.go +++ b/read.go @@ -1,344 +1,340 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "database/sql" "fmt" "html/template" "math" "net/http" "strconv" - "strings" "time" . "github.com/gorilla/feeds" "github.com/gorilla/mux" stripmd "github.com/writeas/go-strip-markdown/v2" "github.com/writeas/impart" "github.com/writeas/web-core/log" "github.com/writeas/web-core/memo" "github.com/writefreely/writefreely/page" ) const ( tlFeedLimit = 100 tlAPIPageLimit = 10 tlMaxAuthorPosts = 5 tlPostsPerPage = 16 tlMaxPostCache = 250 tlCacheDur = 10 * time.Minute ) type localTimeline struct { m *memo.Memo posts *[]PublicPost // Configuration values postsPerPage int } type readPublication struct { page.StaticPage Posts *[]PublicPost CurrentPage int TotalPages int SelTopic string IsAdmin bool CanInvite bool // Customizable page content ContentTitle string Content template.HTML } func initLocalTimeline(app *App) { app.timeline = &localTimeline{ postsPerPage: tlPostsPerPage, m: memo.New(app.FetchPublicPosts, tlCacheDur), } } // satisfies memo.Func func (app *App) FetchPublicPosts() (interface{}, error) { // Conditions limit := fmt.Sprintf("LIMIT %d", tlMaxPostCache) // This is better than the hard limit when limiting posts from individual authors // ageCond := `p.created >= ` + app.db.dateSub(3, "month") + ` AND ` // Finds all public posts and posts in a public collection published during the owner's active subscription period and within the last 3 months rows, err := app.db.Query(`SELECT p.id, c.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated FROM collections c LEFT JOIN posts p ON p.collection_id = c.id LEFT JOIN users u ON u.id = p.owner_id WHERE c.privacy = 1 AND (p.created <= ` + app.db.now() + ` AND pinned_position IS NULL) AND u.status = 0 ORDER BY p.created DESC ` + limit) if err != nil { log.Error("Failed selecting from posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve collection posts." + err.Error()} } defer rows.Close() ap := map[string]uint{} posts := []PublicPost{} for rows.Next() { p := &Post{} c := &Collection{} var alias, title sql.NullString err = rows.Scan(&p.ID, &c.ID, &alias, &title, &p.Slug, &p.Title, &p.Content, &p.Font, &p.Language, &p.RTL, &p.Created, &p.Updated) if err != nil { log.Error("[READ] Unable to scan row, skipping: %v", err) continue } c.hostName = app.cfg.App.Host isCollectionPost := alias.Valid if isCollectionPost { c.Alias = alias.String if c.Alias != "" && ap[c.Alias] == tlMaxAuthorPosts { // Don't add post if we've hit the post-per-author limit continue } c.Public = true c.Title = title.String c.Monetization = app.db.GetCollectionAttribute(c.ID, "monetization_pointer") } p.extractData() p.handlePremiumContent(c, false, false, app.cfg) p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), "", app.cfg)) fp := p.processPost() if isCollectionPost { fp.Collection = &CollectionObj{Collection: *c} } posts = append(posts, fp) ap[c.Alias]++ } return posts, nil } func viewLocalTimelineAPI(app *App, w http.ResponseWriter, r *http.Request) error { updateTimelineCache(app.timeline, false) skip, _ := strconv.Atoi(r.FormValue("skip")) posts := []PublicPost{} for i := skip; i < skip+tlAPIPageLimit && i < len(*app.timeline.posts); i++ { posts = append(posts, (*app.timeline.posts)[i]) } return impart.WriteSuccess(w, posts, http.StatusOK) } func viewLocalTimeline(app *App, w http.ResponseWriter, r *http.Request) error { if !app.cfg.App.LocalTimeline { return impart.HTTPError{http.StatusNotFound, "Page doesn't exist."} } vars := mux.Vars(r) var p int page := 1 p, _ = strconv.Atoi(vars["page"]) if p > 0 { page = p } return showLocalTimeline(app, w, r, page, vars["author"], vars["tag"]) } // updateTimelineCache will reset and update the cache if it is stale or // the boolean passed in is true. func updateTimelineCache(tl *localTimeline, reset bool) { if reset { tl.m.Reset() } // Fetch posts if the cache is empty, has been reset or enough time has // passed since last cache. if tl.posts == nil || reset || tl.m.Invalidate() { log.Info("[READ] Updating post cache") postsInterfaces, err := tl.m.Get() if err != nil { log.Error("[READ] Unable to cache posts: %v", err) } else { castPosts := postsInterfaces.([]PublicPost) tl.posts = &castPosts } } } func showLocalTimeline(app *App, w http.ResponseWriter, r *http.Request, page int, author, tag string) error { updateTimelineCache(app.timeline, false) pl := len(*(app.timeline.posts)) ttlPages := int(math.Ceil(float64(pl) / float64(app.timeline.postsPerPage))) start := 0 if page > 1 { start = app.timeline.postsPerPage * (page - 1) if start > pl { return impart.HTTPError{http.StatusFound, fmt.Sprintf("/read/p/%d", ttlPages)} } } end := app.timeline.postsPerPage * page if end > pl { end = pl } var posts []PublicPost if author != "" { posts = []PublicPost{} for _, p := range *app.timeline.posts { if author == "anonymous" { if p.Collection == nil { posts = append(posts, p) } } else if p.Collection != nil && p.Collection.Alias == author { posts = append(posts, p) } } } else if tag != "" { posts = []PublicPost{} for _, p := range *app.timeline.posts { if p.HasTag(tag) { posts = append(posts, p) } } } else { posts = *app.timeline.posts posts = posts[start:end] } d := &readPublication{ StaticPage: pageForReq(app, r), Posts: &posts, CurrentPage: page, TotalPages: ttlPages, SelTopic: tag, } u := getUserSession(app, r) d.IsAdmin = u != nil && u.IsAdmin() d.CanInvite = canUserInvite(app.cfg, d.IsAdmin) c, err := getReaderSection(app) if err != nil { return err } d.ContentTitle = c.Title.String d.Content = template.HTML(applyMarkdown([]byte(c.Content), "", app.cfg)) err = templates["read"].ExecuteTemplate(w, "base", d) if err != nil { log.Error("Unable to render reader: %v", err) fmt.Fprintf(w, ":(") } return nil } // NextPageURL provides a full URL for the next page of collection posts func (c *readPublication) NextPageURL(n int) string { return fmt.Sprintf("/read/p/%d", n+1) } // PrevPageURL provides a full URL for the previous page of collection posts, // returning a /page/N result for pages >1 func (c *readPublication) PrevPageURL(n int) string { if n == 2 { // Previous page is 1; no need for /p/ prefix return "/read" } return fmt.Sprintf("/read/p/%d", n-1) } // handlePostIDRedirect handles a route where a post ID is given and redirects // the user to the canonical post URL. func handlePostIDRedirect(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) postID := vars["post"] p, err := app.db.GetPost(postID, 0) if err != nil { return err } if !p.CollectionID.Valid { // No collection; send to normal URL // NOTE: not handling single user blogs here since this handler is only used for the Reader - return impart.HTTPError{http.StatusFound, app.cfg.App.Host + "/" + postID + ".md"} + return impart.HTTPError{http.StatusFound, app.cfg.App.AbsoluteHost() + "/" + postID + ".md"} } c, err := app.db.GetCollectionBy("id = ?", fmt.Sprintf("%d", p.CollectionID.Int64)) if err != nil { return err } c.hostName = app.cfg.App.Host // Retrieve collection information and send user to canonical URL return impart.HTTPError{http.StatusFound, c.CanonicalURL() + p.Slug.String} } func viewLocalTimelineFeed(app *App, w http.ResponseWriter, req *http.Request) error { if !app.cfg.App.LocalTimeline { return impart.HTTPError{http.StatusNotFound, "Page doesn't exist."} } - if !strings.HasSuffix(req.URL.Path, "/") { - return impart.HTTPError{http.StatusMovedPermanently, "/read/feed/"} - } updateTimelineCache(app.timeline, false) feed := &Feed{ Title: app.cfg.App.SiteName + " Reader", - Link: &Link{Href: app.cfg.App.Host}, + Link: &Link{Href: app.cfg.App.AbsoluteHost() + "/"}, Description: "Read the latest posts from " + app.cfg.App.SiteName + ".", Created: time.Now(), } c := 0 var title, permalink, author string for _, p := range *app.timeline.posts { if c == tlFeedLimit { break } title = p.PlainDisplayTitle() - permalink = p.CanonicalURL(app.cfg.App.Host) + permalink = p.CanonicalURL(app.cfg.App.AbsoluteHost()) if p.Collection != nil { author = p.Collection.Title } else { author = "Anonymous" } i := &Item{ - Id: app.cfg.App.Host + "/read/a/" + p.ID, + Id: app.cfg.App.AbsoluteHost() + "/read/a/" + p.ID, Title: title, Link: &Link{Href: permalink}, Description: "", Content: applyMarkdown([]byte(p.Content), "", app.cfg), Author: &Author{author, ""}, Created: p.Created, Updated: p.Updated, } feed.Items = append(feed.Items, i) c++ } rss, err := feed.ToRss() if err != nil { return err } fmt.Fprint(w, rss) return nil } diff --git a/routes.go b/routes.go index 7447b62..0de38f4 100644 --- a/routes.go +++ b/routes.go @@ -1,251 +1,283 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "net/http" "net/url" "path/filepath" "strings" "github.com/gorilla/csrf" "github.com/gorilla/mux" "github.com/writeas/go-webfinger" "github.com/writeas/web-core/log" "github.com/writefreely/go-nodeinfo" ) // InitStaticRoutes adds routes for serving static files. // TODO: this should just be a func, not method func (app *App) InitStaticRoutes(r *mux.Router) { // Handle static files fs := http.FileServer(http.Dir(filepath.Join(app.cfg.Server.StaticParentDir, staticDir))) fs = cacheControl(fs) app.shttp = http.NewServeMux() app.shttp.Handle("/", fs) - r.PathPrefix("/").Handler(fs) + staticPrefixes := []string{ + "/css/", + "/fonts/", + "/img/", + "/js/", + "/local/", + } + for _, prefix := range staticPrefixes { + r.PathPrefix(prefix).Handler(fs) + } + r.Handle("/favicon.ico", fs) +} + +// MountSubdirectory mounts the application router under the configured subdirectory. +func MountSubdirectory(apper Apper, r *mux.Router, h http.Handler) { + basePath := apper.App().cfg.App.SubdirectoryPath() + if basePath == "" { + r.PathPrefix("/").Handler(h) + return + } + + r.HandleFunc(basePath, func(w http.ResponseWriter, req *http.Request) { + http.Redirect(w, req, basePath+"/", http.StatusMovedPermanently) + }) + r.PathPrefix(basePath + "/").Handler(http.StripPrefix(basePath, h)) + + // Ensure requests that miss the configured subdirectory are redirected under it. + r.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + target := apper.App().cfg.App.PrefixPath(req.URL.Path) + if req.URL.RawQuery != "" { + target += "?" + req.URL.RawQuery + } + http.Redirect(w, req, target, http.StatusTemporaryRedirect) + }) } // InitRoutes adds dynamic routes for the given mux.Router. func InitRoutes(apper Apper, r *mux.Router) *mux.Router { // Create handler handler := NewWFHandler(apper) // Set up routes hostSubroute := apper.App().cfg.App.Host[strings.Index(apper.App().cfg.App.Host, "://")+3:] if apper.App().cfg.App.SingleUser { hostSubroute = "{domain}" } else { if strings.HasPrefix(hostSubroute, "localhost") { hostSubroute = "localhost" } } if apper.App().cfg.App.SingleUser { log.Info("Adding %s routes (single user)...", hostSubroute) } else { log.Info("Adding %s routes (multi-user)...", hostSubroute) } // Primary app routes write := r.PathPrefix("/").Subrouter() // Federation endpoint configurations wf := webfinger.Default(wfResolver{apper.App().db, apper.App().cfg}) wf.NoTLSHandler = nil // Federation endpoints // host-meta write.HandleFunc("/.well-known/host-meta", handler.Web(handleViewHostMeta, UserLevelReader)) // webfinger write.HandleFunc(webfinger.WebFingerPath, handler.LogHandlerFunc(http.HandlerFunc(wf.Webfinger))) // nodeinfo niCfg := nodeInfoConfig(apper.App().db, apper.App().cfg) ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{apper.App().cfg, apper.App().db}) write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover))) write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo))) // handle mentions write.HandleFunc("/@/{handle}", handler.Web(handleViewMention, UserLevelReader)) configureSlackOauth(handler, write, apper.App()) configureWriteAsOauth(handler, write, apper.App()) configureGitlabOauth(handler, write, apper.App()) configureGenericOauth(handler, write, apper.App()) configureGiteaOauth(handler, write, apper.App()) // Set up dynamic page handlers // Handle auth auth := write.PathPrefix("/api/auth/").Subrouter() if apper.App().cfg.App.OpenRegistration { auth.HandleFunc("/signup", handler.All(apiSignup)).Methods("POST") } auth.HandleFunc("/login", handler.All(login)).Methods("POST") auth.HandleFunc("/read", handler.WebErrors(handleWebCollectionUnlock, UserLevelNone)).Methods("POST") auth.HandleFunc("/me", handler.All(handleAPILogout)).Methods("DELETE") // Handle logged in user sections me := write.PathPrefix("/me").Subrouter() me.HandleFunc("/", handler.Redirect("/me", UserLevelUser)) me.HandleFunc("/c", handler.Redirect("/me/c/", UserLevelUser)).Methods("GET") me.HandleFunc("/c/", handler.User(viewCollections)).Methods("GET") me.HandleFunc("/c/{collection}", handler.User(viewEditCollection)).Methods("GET") me.HandleFunc("/c/{collection}/stats", handler.User(viewStats)).Methods("GET") me.HandleFunc("/c/{collection}/subscribers", handler.User(handleViewSubscribers)).Methods("GET") me.Path("/delete").Handler(csrf.Protect(apper.App().keys.CSRFKey)(handler.User(handleUserDelete))).Methods("POST") me.HandleFunc("/posts", handler.Redirect("/me/posts/", UserLevelUser)).Methods("GET") me.HandleFunc("/posts/", handler.User(viewArticles)).Methods("GET") me.HandleFunc("/posts/export.csv", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET") me.HandleFunc("/posts/export.zip", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET") me.HandleFunc("/posts/export.json", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET") me.HandleFunc("/export", handler.User(viewExportOptions)).Methods("GET") me.HandleFunc("/export.json", handler.Download(viewExportFull, UserLevelUser)).Methods("GET") me.HandleFunc("/import", handler.User(viewImport)).Methods("GET") me.Path("/settings").Handler(csrf.Protect(apper.App().keys.CSRFKey)(handler.User(viewSettings))).Methods("GET") me.HandleFunc("/invites", handler.User(handleViewUserInvites)).Methods("GET") me.HandleFunc("/logout", handler.Web(viewLogout, UserLevelNone)).Methods("GET") write.HandleFunc("/api/me", handler.All(viewMeAPI)).Methods("GET") apiMe := write.PathPrefix("/api/me/").Subrouter() apiMe.HandleFunc("/", handler.All(viewMeAPI)).Methods("GET") apiMe.HandleFunc("/posts", handler.UserWebAPI(viewMyPostsAPI)).Methods("GET") apiMe.HandleFunc("/collections", handler.UserAPI(viewMyCollectionsAPI)).Methods("GET") apiMe.HandleFunc("/password", handler.All(updatePassphrase)).Methods("POST") apiMe.HandleFunc("/self", handler.All(updateSettings)).Methods("POST") apiMe.HandleFunc("/invites", handler.User(handleCreateUserInvite)).Methods("POST") apiMe.HandleFunc("/import", handler.User(handleImport)).Methods("POST") apiMe.HandleFunc("/oauth/remove", handler.User(removeOauth)).Methods("POST") // Sign up validation write.HandleFunc("/api/alias", handler.All(handleUsernameCheck)).Methods("POST") write.HandleFunc("/api/markdown", handler.All(handleRenderMarkdown)).Methods("POST") instanceURL, _ := url.Parse(apper.App().Config().App.Host) host := instanceURL.Host // Handle collections write.HandleFunc("/api/collections", handler.All(newCollection)).Methods("POST") apiColls := write.PathPrefix("/api/collections/").Subrouter() apiColls.HandleFunc("/monetization-pointer", handler.PlainTextAPI(handleSPSPEndpoint)).Methods("GET") apiColls.HandleFunc("/"+host, handler.AllReader(fetchCollection)).Methods("GET") apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.AllReader(fetchCollection)).Methods("GET") apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.All(existingCollection)).Methods("POST", "DELETE") apiColls.HandleFunc("/{alias}/posts", handler.AllReader(fetchCollectionPosts)).Methods("GET") apiColls.HandleFunc("/{alias}/posts", handler.All(newPost)).Methods("POST") apiColls.HandleFunc("/{alias}/posts/{post}", handler.AllReader(fetchPost)).Methods("GET") apiColls.HandleFunc("/{alias}/posts/{post:[a-zA-Z0-9]{10}}", handler.All(existingPost)).Methods("POST") apiColls.HandleFunc("/{alias}/posts/{post}/splitcontent", handler.AllReader(handleGetSplitContent)).Methods("GET", "POST") apiColls.HandleFunc("/{alias}/posts/{post}/{property}", handler.AllReader(fetchPostProperty)).Methods("GET") apiColls.HandleFunc("/{alias}/collect", handler.All(addPost)).Methods("POST") apiColls.HandleFunc("/{alias}/pin", handler.All(pinPost)).Methods("POST") apiColls.HandleFunc("/{alias}/unpin", handler.All(pinPost)).Methods("POST") apiColls.HandleFunc("/{alias}/email/subscribers/export.csv", handler.Download(handleExportEmailSubscriptions, UserLevelUser)).Methods("GET") apiColls.HandleFunc("/{alias}/email/subscribe", handler.All(handleCreateEmailSubscription)).Methods("POST") apiColls.HandleFunc("/{alias}/email/subscribe", handler.All(handleDeleteEmailSubscription)).Methods("DELETE") apiColls.HandleFunc("/{collection}/email/unsubscribe", handler.All(handleDeleteEmailSubscription)).Methods("GET") apiColls.HandleFunc("/{alias}/inbox", handler.All(handleFetchCollectionInbox)).Methods("POST") apiColls.HandleFunc("/{alias}/outbox", handler.AllReader(handleFetchCollectionOutbox)).Methods("GET") apiColls.HandleFunc("/{alias}/following", handler.AllReader(handleFetchCollectionFollowing)).Methods("GET") apiColls.HandleFunc("/{alias}/followers", handler.AllReader(handleFetchCollectionFollowers)).Methods("GET") // Handle posts write.HandleFunc("/api/posts", handler.All(newPost)).Methods("POST") posts := write.PathPrefix("/api/posts/").Subrouter() posts.HandleFunc("/claim", handler.All(addPost)).Methods("POST") posts.HandleFunc("/disperse", handler.All(dispersePost)).Methods("POST") posts.HandleFunc("/{post:[a-zA-Z0-9]+}", handler.AllReader(fetchPost)).Methods("GET") posts.HandleFunc("/{post:[a-zA-Z0-9]+}", handler.All(existingPost)).Methods("POST", "PUT") posts.HandleFunc("/{post:[a-zA-Z0-9]+}", handler.All(deletePost)).Methods("DELETE") posts.HandleFunc("/{post:[a-zA-Z0-9]+}/{property}", handler.AllReader(fetchPostProperty)).Methods("GET") write.HandleFunc("/auth/signup", handler.Web(handleWebSignup, UserLevelNoneRequired)).Methods("POST") write.HandleFunc("/auth/login", handler.Web(webLogin, UserLevelNoneRequired)).Methods("POST") write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET") write.HandleFunc("/admin/monitor", handler.Admin(handleViewAdminMonitor)).Methods("GET") write.HandleFunc("/admin/settings", handler.Admin(handleViewAdminSettings)).Methods("GET") write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET") write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET") write.HandleFunc("/admin/user/{username}/delete", handler.Admin(handleAdminDeleteUser)).Methods("POST") write.HandleFunc("/admin/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).Methods("POST") write.HandleFunc("/admin/user/{username}/passphrase", handler.Admin(handleAdminResetUserPass)).Methods("POST") write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET") write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET") write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST") write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST") write.HandleFunc("/admin/updates", handler.Admin(handleViewAdminUpdates)).Methods("GET") // Handle special pages first write.Path("/reset").Handler(csrf.Protect(apper.App().keys.CSRFKey)(handler.Web(viewResetPassword, UserLevelNoneRequired))) write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired)) write.HandleFunc("/signup", handler.Web(handleViewLanding, UserLevelNoneRequired)) write.HandleFunc("/invite/{code:[a-zA-Z0-9]+}", handler.Web(handleViewInvite, UserLevelOptional)).Methods("GET") // TODO: show a reader-specific 404 page if the function is disabled write.HandleFunc("/read", handler.Web(viewLocalTimeline, UserLevelReader)) RouteRead(handler, UserLevelReader, write.PathPrefix("/read").Subrouter()) draftEditPrefix := "" if apper.App().cfg.App.SingleUser { draftEditPrefix = "/d" write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelUser)).Methods("GET") } else { write.HandleFunc("/new", handler.Web(handleViewPad, UserLevelUser)).Methods("GET") } // All the existing stuff write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelUser)).Methods("GET") write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelUser)).Methods("GET") // Collections if apper.App().cfg.App.SingleUser { RouteCollections(handler, write.PathPrefix("/").Subrouter()) } else { write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelReader)) write.HandleFunc("/{collection}/", handler.Web(handleViewCollection, UserLevelReader)) RouteCollections(handler, write.PathPrefix("/{prefix:[@~$!\\-+]?}{collection}").Subrouter()) // Posts } write.HandleFunc(draftEditPrefix+"/{post}", handler.Web(handleViewPost, UserLevelOptional)) write.HandleFunc("/", handler.Web(handleViewHome, UserLevelOptional)) return r } func RouteCollections(handler *Handler, r *mux.Router) { r.HandleFunc("/logout", handler.Web(handleLogOutCollection, UserLevelOptional)) r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader)) r.HandleFunc("/archive/", handler.Web(handleViewCollection, UserLevelReader)) r.HandleFunc("/{archive:archive}/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader)) r.HandleFunc("/lang:{lang:[a-z]{2}}", handler.Web(handleViewCollectionLang, UserLevelOptional)) r.HandleFunc("/lang:{lang:[a-z]{2}}/page/{page:[0-9]+}", handler.Web(handleViewCollectionLang, UserLevelOptional)) r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelReader)) r.HandleFunc("/tag:{tag}/page/{page:[0-9]+}", handler.Web(handleViewCollectionTag, UserLevelReader)) r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelReader)) r.HandleFunc("/sitemap.xml", handler.AllReader(handleViewSitemap)) r.HandleFunc("/feed/", handler.AllReader(ViewFeed)) r.HandleFunc("/email/confirm/{subscriber}", handler.All(handleConfirmEmailSubscription)).Methods("GET") r.HandleFunc("/email/unsubscribe/{subscriber}", handler.All(handleDeleteEmailSubscription)).Methods("GET") r.HandleFunc("/{slug}", handler.CollectionPostOrStatic) r.HandleFunc("/{slug}/edit", handler.Web(handleViewPad, UserLevelUser)) r.HandleFunc("/{slug}/edit/meta", handler.Web(handleViewMeta, UserLevelUser)) r.HandleFunc("/{slug}/", handler.Web(handleCollectionPostRedirect, UserLevelReader)).Methods("GET") } func RouteRead(handler *Handler, readPerm UserLevelFunc, r *mux.Router) { r.HandleFunc("/api/posts", handler.Web(viewLocalTimelineAPI, readPerm)) r.HandleFunc("/p/{page}", handler.Web(viewLocalTimeline, readPerm)) - r.HandleFunc("/feed", handler.Web(viewLocalTimelineFeed, readPerm)) r.HandleFunc("/feed/", handler.Web(viewLocalTimelineFeed, readPerm)) r.HandleFunc("/t/{tag}", handler.Web(viewLocalTimeline, readPerm)) r.HandleFunc("/a/{post}", handler.Web(handlePostIDRedirect, readPerm)) r.HandleFunc("/{author}", handler.Web(viewLocalTimeline, readPerm)) r.HandleFunc("/", handler.Web(viewLocalTimeline, readPerm)) } diff --git a/routes_test.go b/routes_test.go index 9669e6e..fc3b3fa 100644 --- a/routes_test.go +++ b/routes_test.go @@ -1,38 +1,308 @@ package writefreely import ( "net/http" "net/http/httptest" + "sort" "strings" "testing" "github.com/gorilla/mux" ) +func newSubdirTestRouter(t *testing.T, subdir string) *mux.Router { + t.Helper() + + app := NewApp("testdata/config.ini") + if err := app.LoadConfig(); err != nil { + t.Fatalf("Could not create an app; %v", err) + } + app.cfg.App.Subdirectory = subdir + + router := mux.NewRouter() + inner := mux.NewRouter() + inner.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }) + + MountSubdirectory(app, router, inner) + return router +} + func TestCacheControlForStaticFiles(t *testing.T) { app := NewApp("testdata/config.ini") if err := app.LoadConfig(); err != nil { t.Fatalf("Could not create an app; %v", err) } router := mux.NewRouter() app.InitStaticRoutes(router) rec := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/style.css", nil) + req := httptest.NewRequest("GET", "/css/write.css", nil) router.ServeHTTP(rec, req) if code := rec.Result().StatusCode; code != http.StatusOK { - t.Fatalf("Could not get /style.css, got HTTP status %d", code) + t.Fatalf("Could not get /css/write.css, got HTTP status %d", code) } actual := rec.Result().Header.Get("Cache-Control") expectedDirectives := []string{ "public", "max-age", "immutable", } for _, expected := range expectedDirectives { if !strings.Contains(actual, expected) { t.Errorf("Expected Cache-Control header to contain '%s', but was '%s'", expected, actual) } } } + +func TestMountSubdirectoryRedirectsMissingPrefix(t *testing.T) { + router := newSubdirTestRouter(t, "/blog") + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/hello?x=1", nil) + router.ServeHTTP(rec, req) + + if code := rec.Result().StatusCode; code != http.StatusTemporaryRedirect { + t.Fatalf("Expected HTTP status %d, got %d", http.StatusTemporaryRedirect, code) + } + if loc := rec.Result().Header.Get("Location"); loc != "/blog/hello?x=1" { + t.Fatalf("Expected redirect location '/blog/hello?x=1', got '%s'", loc) + } +} + +func TestMountSubdirectoryServesPrefixedPath(t *testing.T) { + router := newSubdirTestRouter(t, "/blog") + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/blog/hello", nil) + router.ServeHTTP(rec, req) + + if code := rec.Result().StatusCode; code != http.StatusOK { + t.Fatalf("Expected HTTP status %d, got %d", http.StatusOK, code) + } + if body := rec.Body.String(); body != "ok" { + t.Fatalf("Expected body 'ok', got '%s'", body) + } +} + +func discoverRouteTemplates(t *testing.T) []string { + t.Helper() + + app := NewApp("testdata/config.ini") + if err := app.LoadConfig(); err != nil { + t.Fatalf("Could not create an app; %v", err) + } + app.cfg.App.Host = "https://example.com" + initKeyPaths(app) + if err := app.LoadKeys(); err != nil { + t.Fatalf("Could not load app keys; %v", err) + } + + router := mux.NewRouter() + InitRoutes(app, router) + + seen := map[string]struct{}{} + err := router.Walk(func(route *mux.Route, _ *mux.Router, _ []*mux.Route) error { + tmpl, err := route.GetPathTemplate() + if err != nil { + return nil + } + seen[tmpl] = struct{}{} + return nil + }) + if err != nil { + t.Fatalf("unable to walk routes: %v", err) + } + + templates := make([]string, 0, len(seen)) + for tmpl := range seen { + templates = append(templates, tmpl) + } + sort.Strings(templates) + return templates +} + +func sampleTemplateVarValue(inner string) string { + name := inner + pattern := "" + if i := strings.Index(inner, ":"); i > -1 { + name = inner[:i] + pattern = inner[i+1:] + } + + switch { + case name == "prefix": + return "@" + case name == "page": + return "1" + case name == "lang": + return "en" + case name == "archive": + return "archive" + case name == "post" && strings.Contains(pattern, "{10}"): + return "abcdefghij" + case name == "post": + return "samplepost" + case name == "tag": + return "testing" + case name == "handle": + return "alice" + case name == "slug": + return "sample" + case name == "collection": + return "sample" + case name == "alias": + return "sample" + default: + return "sample" + } +} + +func samplePathFromTemplate(tmpl string) string { + var out strings.Builder + for i := 0; i < len(tmpl); { + if tmpl[i] != '{' { + out.WriteByte(tmpl[i]) + i++ + continue + } + + j := i + 1 + depth := 1 + for ; j < len(tmpl) && depth > 0; j++ { + switch tmpl[j] { + case '{': + depth++ + case '}': + depth-- + } + } + if depth != 0 { + out.WriteByte(tmpl[i]) + i++ + continue + } + + inner := tmpl[i+1 : j-1] + out.WriteString(sampleTemplateVarValue(inner)) + i = j + } + + replaced := out.String() + for strings.Contains(replaced, "//") { + replaced = strings.ReplaceAll(replaced, "//", "/") + } + return replaced +} + +func TestInitRoutesDiscoveryFindsEndpoints(t *testing.T) { + templates := discoverRouteTemplates(t) + + if len(templates) < 40 { + t.Fatalf("expected at least 40 route templates, got %d", len(templates)) + } + + required := map[string]bool{ + "/": false, + "/api/posts": false, + } + for _, tmpl := range templates { + if _, ok := required[tmpl]; ok { + required[tmpl] = true + } + } + for tmpl, found := range required { + if !found { + t.Fatalf("expected discovered route template %q", tmpl) + } + } +} + +func TestDiscoveredEndpointsMountUnderSubdirectory(t *testing.T) { + templates := discoverRouteTemplates(t) + + inner := mux.NewRouter() + for _, tmpl := range templates { + inner.HandleFunc(tmpl, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + } + + app := NewApp("testdata/config.ini") + if err := app.LoadConfig(); err != nil { + t.Fatalf("Could not create an app; %v", err) + } + app.cfg.App.Subdirectory = "/blog" + + router := mux.NewRouter() + MountSubdirectory(app, router, inner) + + for _, tmpl := range templates { + path := samplePathFromTemplate(tmpl) + + // Requests without configured subdirectory should redirect under it. + redir := httptest.NewRecorder() + redirReq := httptest.NewRequest("GET", path, nil) + router.ServeHTTP(redir, redirReq) + if redir.Result().StatusCode != http.StatusTemporaryRedirect { + t.Fatalf("expected redirect for %q, got %d", path, redir.Result().StatusCode) + } + + // Requests under configured subdirectory should be served by inner router. + prefixed := app.cfg.App.PrefixPath(path) + okRec := httptest.NewRecorder() + okReq := httptest.NewRequest("GET", prefixed, nil) + router.ServeHTTP(okRec, okReq) + if okRec.Result().StatusCode == http.StatusNotFound || okRec.Result().StatusCode >= 500 { + t.Fatalf("expected mounted route for template %q (sample %q), got %d", tmpl, prefixed, okRec.Result().StatusCode) + } + } +} + +func TestRoutingModesRootSubdirectoryAndSubdomain(t *testing.T) { + // Root and subdomain modes both have no subdirectory mount. + for _, host := range []string{"https://example.com", "https://blog.example.com"} { + app := NewApp("testdata/config.ini") + if err := app.LoadConfig(); err != nil { + t.Fatalf("Could not create an app; %v", err) + } + app.cfg.App.Host = host + app.cfg.App.Subdirectory = "" + + router := mux.NewRouter() + inner := mux.NewRouter() + inner.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + MountSubdirectory(app, router, inner) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/hello", nil) + router.ServeHTTP(rec, req) + if rec.Result().StatusCode != http.StatusOK { + t.Fatalf("expected root-style mount for host %q, got %d", host, rec.Result().StatusCode) + } + } + + // Subdirectory mode redirects missing prefix and serves prefixed routes. + router := newSubdirTestRouter(t, "/blog") + redir := httptest.NewRecorder() + redirReq := httptest.NewRequest("GET", "/hello", nil) + router.ServeHTTP(redir, redirReq) + if redir.Result().StatusCode != http.StatusTemporaryRedirect { + t.Fatalf("expected redirect in subdirectory mode, got %d", redir.Result().StatusCode) + } + if loc := redir.Result().Header.Get("Location"); loc != "/blog/hello" { + t.Fatalf("expected /blog/hello redirect, got %q", loc) + } + + okRec := httptest.NewRecorder() + okReq := httptest.NewRequest("GET", "/blog/hello", nil) + router.ServeHTTP(okRec, okReq) + if okRec.Result().StatusCode != http.StatusOK { + t.Fatalf("expected served prefixed route in subdirectory mode, got %d", okRec.Result().StatusCode) + } +} diff --git a/session.go b/session.go index 9f8bbba..03d3963 100644 --- a/session.go +++ b/session.go @@ -1,147 +1,146 @@ /* * Copyright © 2018-2019 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "encoding/gob" - "net/http" - "strings" - "github.com/gorilla/sessions" "github.com/writeas/web-core/log" + "net/http" + "strings" ) const ( day = 86400 sessionLength = 180 * day userEmailCookieName = "ue" userEmailCookieVal = "email" cookieName = "wfu" cookieUserVal = "u" blogPassCookieName = "ub" ) // InitSession creates the cookie store. It depends on the keychain already // being loaded. func (app *App) InitSession() { // Register complex data types we'll be storing in cookies gob.Register(&User{}) // Create the cookie store store := sessions.NewCookieStore(app.keys.CookieAuthKey, app.keys.CookieKey) store.Options = &sessions.Options{ Path: "/", MaxAge: sessionLength, HttpOnly: true, Secure: strings.HasPrefix(app.cfg.App.Host, "https://"), } if store.Options.Secure { store.Options.SameSite = http.SameSiteNoneMode } app.sessionStore = store } func getSessionFlashes(app *App, w http.ResponseWriter, r *http.Request, session *sessions.Session) ([]string, error) { var err error if session == nil { session, err = app.sessionStore.Get(r, cookieName) if err != nil { return nil, err } } f := []string{} if flashes := session.Flashes(); len(flashes) > 0 { for _, flash := range flashes { if str, ok := flash.(string); ok { f = append(f, str) } } } saveUserSession(app, r, w) return f, nil } func addSessionFlash(app *App, w http.ResponseWriter, r *http.Request, m string, session *sessions.Session) error { var err error if session == nil { session, err = app.sessionStore.Get(r, cookieName) } if err != nil { log.Error("Unable to add flash '%s': %v", m, err) return err } session.AddFlash(m) saveUserSession(app, r, w) return nil } func getUserAndSession(app *App, r *http.Request) (*User, *sessions.Session) { session, err := app.sessionStore.Get(r, cookieName) if err == nil { // Got the currently logged-in user val := session.Values[cookieUserVal] var u = &User{} var ok bool if u, ok = val.(*User); ok { return u, session } } return nil, nil } func getUserSession(app *App, r *http.Request) *User { u, _ := getUserAndSession(app, r) return u } func saveUserSession(app *App, r *http.Request, w http.ResponseWriter) error { session, err := app.sessionStore.Get(r, cookieName) if err != nil { return ErrInternalCookieSession } // Extend the session session.Options.MaxAge = int(sessionLength) // Remove any information that accidentally got added // FIXME: find where Plan information is getting saved to cookie. val := session.Values[cookieUserVal] var u = &User{} var ok bool if u, ok = val.(*User); ok { session.Values[cookieUserVal] = u.Cookie() } err = session.Save(r, w) if err != nil { log.Error("Couldn't saveUserSession: %v", err) } return err } func getFullUserSession(app *App, r *http.Request) (*User, error) { u := getUserSession(app, r) if u == nil { return nil, nil } var err error u, err = app.db.GetUserByID(u.ID) return u, err } diff --git a/spam/email.go b/spam/email.go index ca8ac94..de017ab 100644 --- a/spam/email.go +++ b/spam/email.go @@ -1,44 +1,43 @@ /* * Copyright © 2020-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package spam import ( - "strings" - "github.com/writeas/web-core/id" + "strings" ) var honeypotField string func HoneypotFieldName() string { if honeypotField == "" { honeypotField = id.Generate62RandomString(39) } return honeypotField } // CleanEmail takes an email address and strips it down to a unique address that can be blocked. func CleanEmail(email string) string { emailParts := strings.Split(strings.ToLower(email), "@") if len(emailParts) < 2 { return "" } u := emailParts[0] d := emailParts[1] // Ignore anything after '+' plusIdx := strings.IndexRune(u, '+') if plusIdx > -1 { u = u[:plusIdx] } // Strip dots in email address u = strings.ReplaceAll(u, ".", "") return u + "@" + d } diff --git a/static/js/modals.js b/static/js/modals.js index 912df8a..0910d7e 100644 --- a/static/js/modals.js +++ b/static/js/modals.js @@ -1,24 +1,24 @@ /* * Copyright © 2016-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ function showModal(id) { document.getElementById('overlay').style.display = 'block'; document.getElementById('modal-'+id).style.display = 'block'; } var closeModals = function(e) { e.preventDefault(); document.getElementById('overlay').style.display = 'none'; var modals = document.querySelectorAll('.modal'); for (var i=0; i closeModals(e)); +H.getEl('overlay').on('click', closeModals); \ No newline at end of file diff --git a/static/js/postactions.js b/static/js/postactions.js index 74e3223..fbac850 100644 --- a/static/js/postactions.js +++ b/static/js/postactions.js @@ -1,121 +1,122 @@ var postActions = function() { + var basePath = window.WF_BASE_PATH || ""; var $container = He.get('moving'); var MultiMove = function(el, id, singleUser) { var lbl = el.options[el.selectedIndex].textContent; var collAlias = el.options[el.selectedIndex].value; var $lbl = He.$('label[for=move-'+id+']')[0]; $lbl.textContent = "moving to "+lbl+"..."; var params; if (collAlias == '|anonymous|') { params = [id]; } else { params = [{ id: id }]; } var callback = function(code, resp) { if (code == 200) { for (var i=0; i"; var pre = "/"+collAlias; if (typeof singleUser !== 'undefined' && singleUser) { pre = ""; } var newPostURL = pre+"/"+resp.data[i].post.slug; try { // Posts page He.$('#post-'+resp.data[i].post.id+' > h3 > a')[0].href = newPostURL; } catch (e) { // Blog index var $article = He.get('post-'+resp.data[i].post.id); $article.className = 'norm moved'; if (collAlias == '|anonymous|') { var draftPre = ""; if (typeof singleUser !== 'undefined' && singleUser) { draftPre = "d/"; } - $article.innerHTML = '

Unpublished post.

'; + $article.innerHTML = '

Unpublished post.

'; } else { $article.innerHTML = '

Moved to '+lbl+'.

'; } } } else { $lbl.innerHTML = "unable to move: "+resp.data[i].error_msg; } } } }; if (collAlias == '|anonymous|') { - He.postJSON("/api/posts/disperse", params, callback); + He.postJSON(basePath + "{{subdir}}/api/posts/disperse", params, callback); } else { - He.postJSON("/api/collections/"+collAlias+"/collect", params, callback); + He.postJSON(basePath + "{{subdir}}/api/collections/"+collAlias+"/collect", params, callback); } }; var Move = function(el, id, collAlias, singleUser) { var lbl = el.textContent; try { var m = lbl.match(/move to (.*)/); lbl = m[1]; } catch (e) { if (collAlias == '|anonymous|') { lbl = "draft"; } } el.textContent = "moving to "+lbl+"..."; if (collAlias == '|anonymous|') { params = [id]; } else { params = [{ id: id }]; } var callback = function(code, resp) { if (code == 200) { for (var i=0; i"; el.onclick = null; var pre = "/"+collAlias; if (typeof singleUser !== 'undefined' && singleUser) { pre = ""; } var newPostURL = pre+"/"+resp.data[i].post.slug; el.href = newPostURL; el.title = "View on "+lbl; try { // Posts page He.$('#post-'+resp.data[i].post.id+' > h3 > a')[0].href = newPostURL; } catch (e) { // Blog index var $article = He.get('post-'+resp.data[i].post.id); $article.className = 'norm moved'; if (collAlias == '|anonymous|') { var draftPre = ""; if (typeof singleUser !== 'undefined' && singleUser) { draftPre = "d/"; } - $article.innerHTML = '

Unpublished post.

'; + $article.innerHTML = '

Unpublished post.

'; } else { $article.innerHTML = '

Moved to '+lbl+'.

'; } } } else { el.innerHTML = "unable to move: "+resp.data[i].error_msg; } } } } if (collAlias == '|anonymous|') { - He.postJSON("/api/posts/disperse", params, callback); + He.postJSON(basePath + "{{subdir}}/api/posts/disperse", params, callback); } else { - He.postJSON("/api/collections/"+collAlias+"/collect", params, callback); + He.postJSON(basePath + "{{subdir}}/api/collections/"+collAlias+"/collect", params, callback); } }; return { move: Move, multiMove: MultiMove, }; }(); diff --git a/static/js/posts.js b/static/js/posts.js index d44f19c..da07059 100644 --- a/static/js/posts.js +++ b/static/js/posts.js @@ -1,333 +1,335 @@ /** * Functionality for managing local Write.as posts. * * Dependencies: * h.js */ function toggleTheme() { var btns; try { btns = Array.prototype.slice.call(document.getElementById('belt').querySelectorAll('.tool img')); } catch (e) {} if (document.body.className == 'light') { document.body.className = 'dark'; try { for (var i=0; iMore...

'; return $more; }; var localPosts = function() { var $delPost, lastDelPost, lastInfoHTML; var $info = He.get('unsynced-posts-info'); var findPostIdx = function(id) { for (var i=0; i -1) { lastDelPost = posts.splice(i, 1)[0]; $delPost = H.getEl('post-'+id); $delPost.setClass('del-undo'); var $unsyncPosts = document.getElementById('unsynced-posts'); var visible = $unsyncPosts.children.length; for (var i=0; i < $unsyncPosts.children.length; i++) { // NOTE: *.children support in IE9+ if ($unsyncPosts.children[i].className.indexOf('del-undo') !== -1) { visible--; } } if (visible == 0) { H.getEl('unsynced-posts-header').hide(); // TODO: fix undo functionality and don't do the following: H.getEl('unsynced-posts-info').hide(); } H.set('posts', JSON.stringify(posts)); // TODO: fix undo functionality and re-add //lastInfoHTML = $info.innerHTML; //$info.innerHTML = 'Unsynced entry deleted. Undo.'; } }; var UndoDelete = function() { // TODO: fix this header reappearing H.getEl('unsynced-posts-header').show(); $delPost.removeClass('del-undo'); $info.innerHTML = lastInfoHTML; }; return { dismissError: DismissError, deletePost: DeletePostLocal, undoDelete: UndoDelete, }; }(); var movePostHTML = function(postID) { let $tmpl = document.getElementById('move-tmpl'); if ($tmpl === null) { return ""; } return $tmpl.innerHTML.replace(/POST_ID/g, postID); } var createPostEl = function(post, owned, singleUser) { + var basePath = window.WF_BASE_PATH || ""; var $post = document.createElement('div'); let p = H.createPost(post.id, "", post.body) var title = (post.title || p.title || post.id); title = title.replace(/' + title + ''; var posted = ""; if (post.created) { posted = getFormattedDate(new Date(post.created)) } var hasDraft = H.exists('draft' + post.id); - $post.innerHTML += '

' + posted + ' edit' + (hasDraft ? 'ed' : '') + ' delete '+movePostHTML(post.id)+'

'; + $post.innerHTML += '

' + posted + ' edit' + (hasDraft ? 'ed' : '') + ' delete '+movePostHTML(post.id)+'

'; if (post.error) { $post.innerHTML += '

Sync error: ' + post.error + '

'; } if (post.summary) { // TODO: switch to using p.summary, after ensuring it matches summary generated on the backend. $post.innerHTML += '

' + post.summary.replace(/'; } else if (post.body) { var preview; if (post.body.length > 140) { preview = post.body.substr(0, 140) + '...'; } else { preview = post.body; } $post.innerHTML += '

' + preview.replace(/'; } return $post; }; var loadPage = function(p, loadAll) { if (loadAll) { $posts.el.innerHTML = ''; } var startPost = posts.length - 1 - (loadAll ? 0 : ((p-1)*postsPerPage)); var endPost = posts.length - 1 - (p*postsPerPage); for (var i=startPost; i>=0 && i>endPost; i--) { $posts.el.appendChild(createPostEl(posts[i])); } if (loadAll) { if (p < pages) { $posts.el.appendChild(createMorePostsEl()); } } else { var $moreEl = document.getElementById('more-posts'); $moreEl.parentNode.removeChild($moreEl); } try { postsLoaded(posts.length); } catch (e) {} }; var getPageNum = function(url) { var hash; if (url) { hash = url.substr(url.indexOf('#')+1); } else { hash = window.location.hash.substr(1); } var page = hash || 1; page = parseInt(page); if (isNaN(page)) { page = 1; } return page; }; var postsPerPage = 10; var pages = 0; var page = getPageNum(); window.addEventListener('hashchange', function(e) { var newPage = getPageNum(); var didPageIncrement = newPage == getPageNum(e.oldURL) + 1; loadPage(newPage, !didPageIncrement); }); var deletePost = function(postID, token, callback) { deleting = true; var $delBtn = document.getElementById('post-' + postID).getElementsByClassName('delete action')[0]; $delBtn.innerHTML = '...'; var http = new XMLHttpRequest(); - var url = "/api/posts/" + postID + (typeof token !== 'undefined' ? "?token=" + encodeURIComponent(token) : ''); + var basePath = window.WF_BASE_PATH || ""; + var url = basePath + "/api/posts/" + postID + (typeof token !== 'undefined' ? "?token=" + encodeURIComponent(token) : ''); http.open("DELETE", url, true); http.onreadystatechange = function() { if (http.readyState == 4) { deleting = false; if (http.status == 204 || http.status == 404) { for (var i=0; i'; }; if (posts.length == 0) { displayNoPosts(); } else { initialListPop(); } diff --git a/static/js/webmonetization.js b/static/js/webmonetization.js index 0e5faa7..e2833b1 100644 --- a/static/js/webmonetization.js +++ b/static/js/webmonetization.js @@ -1,94 +1,95 @@ /* * Copyright © 2020-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ let unlockingSplitContent = false; let unlockedSplitContent = false; let pendingSplitContent = false; function showWMPaywall($content, $split) { let $readmoreSell = document.createElement('div') $readmoreSell.id = 'readmore-sell'; $content.insertAdjacentElement('beforeend', $readmoreSell); $readmoreSell.appendChild($split); $readmoreSell.insertAdjacentHTML("beforeend", '\n\n

For $5 per month, you can read this and other great writing across our site and other websites that support Web Monetization.

') $readmoreSell.insertAdjacentHTML("beforeend", '\n\n

Get started Learn more

') } function initMonetization() { let $content = document.querySelector('.e-content') let $post = document.getElementById('post-body') let $split = $post.querySelector('.split') if (document.monetization === undefined || $split == null) { if ($split) { showWMPaywall($content, $split) } return } document.monetization.addEventListener('monetizationstop', function(event) { if (pendingSplitContent) { // We've seen the 'pending' activity, so we can assume things will work document.monetization.removeEventListener('monetizationstop', progressHandler) return } // We're getting 'stop' without ever starting, so display the paywall. showWMPaywall($content, $split) }); document.monetization.addEventListener('monetizationpending', function (event) { pendingSplitContent = true }) let progressHandler = function(event) { if (unlockedSplitContent) { document.monetization.removeEventListener('monetizationprogress', progressHandler) return } if (!unlockingSplitContent && !unlockedSplitContent) { unlockingSplitContent = true getSplitContent(event.detail.receipt, function (status, data) { unlockingSplitContent = false if (status == 200) { $split.textContent = "Your subscriber perks start here." $split.insertAdjacentHTML("afterend", "\n\n"+data.data.html_body) } else { $split.textContent = "Something went wrong while unlocking subscriber content." } unlockedSplitContent = true }) } } function getSplitContent(receipt, callback) { let params = "receipt="+encodeURIComponent(receipt) + let basePath = window.WF_BASE_PATH || "" let http = new XMLHttpRequest(); - http.open("POST", "/api/collections/" + window.collAlias + "/posts/" + window.postSlug + "/splitcontent", true); + http.open("POST", basePath + "/api/collections/" + window.collAlias + "/posts/" + window.postSlug + "/splitcontent", true); // Send the proper header information along with the request http.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); http.onreadystatechange = function () { if (http.readyState == 4) { callback(http.status, JSON.parse(http.responseText)); } } http.send(params); } document.monetization.addEventListener('monetizationstart', function() { if (!unlockedSplitContent) { $split.textContent = "Unlocking subscriber content..." } document.monetization.removeEventListener('monetizationstart', progressHandler) }); document.monetization.addEventListener('monetizationprogress', progressHandler); } \ No newline at end of file diff --git a/templates.go b/templates.go index 484bb99..67e1a68 100644 --- a/templates.go +++ b/templates.go @@ -1,240 +1,250 @@ /* * Copyright © 2018-2021 Musing Studio LLC. * * This file is part of WriteFreely. * * WriteFreely is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, included * in the LICENSE file in this source code package. */ package writefreely import ( "errors" "html/template" "io" "net/http" "os" "path/filepath" "strings" "github.com/dustin/go-humanize" "github.com/writeas/web-core/l10n" "github.com/writeas/web-core/log" "github.com/writefreely/writefreely/config" ) var ( - templates = map[string]*template.Template{} - pages = map[string]*template.Template{} - userPages = map[string]*template.Template{} - funcMap = template.FuncMap{ + templates = map[string]*template.Template{} + pages = map[string]*template.Template{} + userPages = map[string]*template.Template{} + templateSubdirectory = "" + funcMap = template.FuncMap{ "largeNumFmt": largeNumFmt, "pluralize": pluralize, "isRTL": isRTL, "isLTR": isLTR, "localstr": localStr, "localhtml": localHTML, "tolower": strings.ToLower, "title": strings.Title, "hasPrefix": strings.HasPrefix, "hasSuffix": strings.HasSuffix, + "subdir": templateSubdir, "dict": dict, } ) const ( templatesDir = "templates" pagesDir = "pages" ) func showUserPage(w http.ResponseWriter, name string, obj interface{}) { if obj == nil { log.Error("showUserPage: data is nil!") return } if err := userPages[filepath.Join("user", name+".tmpl")].ExecuteTemplate(w, name, obj); err != nil { log.Error("Error parsing %s: %v", name, err) } } func initTemplate(parentDir, name string) { if debugging { log.Info(" " + filepath.Join(parentDir, templatesDir, name+".tmpl")) } files := []string{ filepath.Join(parentDir, templatesDir, name+".tmpl"), + filepath.Join(parentDir, templatesDir, "include", "head-base.tmpl"), filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"), } if name == "collection" || name == "collection-tags" || name == "collection-archive" || name == "chorus-collection" || name == "read" { // These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl" files = append(files, filepath.Join(parentDir, templatesDir, "include", "posts.tmpl")) } if name == "chorus-collection" || name == "chorus-collection-post" { files = append(files, filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl")) } if name == "collection" || name == "collection-tags" || name == "collection-archive" || name == "collection-post" || name == "post" || name == "chorus-collection" || name == "chorus-collection-post" { files = append(files, filepath.Join(parentDir, templatesDir, "include", "post-render.tmpl")) } templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...)) } func initPage(parentDir, path, key string) { if debugging { log.Info(" [%s] %s", key, path) } files := []string{ path, + filepath.Join(parentDir, templatesDir, "include", "head-base.tmpl"), filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"), } if key == "login.tmpl" || key == "landing.tmpl" || key == "signup.tmpl" { files = append(files, filepath.Join(parentDir, templatesDir, "include", "oauth.tmpl")) } pages[key] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...)) } func initUserPage(parentDir, path, key string) { if debugging { log.Info(" [%s] %s", key, path) } userPages[key] = template.Must(template.New(key).Funcs(funcMap).ParseFiles( path, + filepath.Join(parentDir, templatesDir, "include", "head-base.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "nav.tmpl"), )) } // InitTemplates loads all template files from the configured parent dir. func InitTemplates(cfg *config.Config) error { log.Info("Loading templates...") + templateSubdirectory = cfg.App.SubdirectoryPath() tmplFiles, err := os.ReadDir(filepath.Join(cfg.Server.TemplatesParentDir, templatesDir)) if err != nil { return err } for _, f := range tmplFiles { if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") { parts := strings.Split(f.Name(), ".") key := parts[0] initTemplate(cfg.Server.TemplatesParentDir, key) } } log.Info("Loading pages...") // Initialize all static pages that use the base template err = filepath.Walk(filepath.Join(cfg.Server.PagesParentDir, pagesDir), func(path string, i os.FileInfo, err error) error { if err != nil { return err } if !i.IsDir() && !strings.HasPrefix(i.Name(), ".") { key := i.Name() initPage(cfg.Server.PagesParentDir, path, key) } return nil }) if err != nil { return err } log.Info("Loading user pages...") // Initialize all user pages that use base templates err = filepath.Walk(filepath.Join(cfg.Server.TemplatesParentDir, templatesDir, "user"), func(path string, f os.FileInfo, err error) error { if err != nil { return err } if !f.IsDir() && !strings.HasPrefix(f.Name(), ".") { corePath := path if cfg.Server.TemplatesParentDir != "" { corePath = corePath[len(cfg.Server.TemplatesParentDir)+1:] } parts := strings.Split(corePath, string(filepath.Separator)) key := f.Name() if len(parts) > 2 { key = filepath.Join(parts[1], f.Name()) } initUserPage(cfg.Server.TemplatesParentDir, path, key) } return nil }) if err != nil { return err } return nil } +func templateSubdir() string { + return templateSubdirectory +} + // renderPage retrieves the given template and renders it to the given io.Writer. // If something goes wrong, the error is logged and returned. func renderPage(w io.Writer, tmpl string, data interface{}) error { err := pages[tmpl].ExecuteTemplate(w, "base", data) if err != nil { log.Error("%v", err) } return err } func largeNumFmt(n int64) string { return humanize.Comma(n) } func pluralize(singular, plural string, n int64) string { if n == 1 { return singular } return plural } func isRTL(d string) bool { return d == "rtl" } func isLTR(d string) bool { return d == "ltr" || d == "auto" } func localStr(term, lang string) string { s := l10n.Strings(lang)[term] if s == "" { s = l10n.Strings("")[term] } return s } func localHTML(term, lang string) template.HTML { s := l10n.Strings(lang)[term] if s == "" { s = l10n.Strings("")[term] } s = strings.Replace(s, "write.as", "writefreely", 1) return template.HTML(s) } // from: https://stackoverflow.com/a/18276968/1549194 func dict(values ...interface{}) (map[string]interface{}, error) { if len(values)%2 != 0 { return nil, errors.New("dict: invalid number of parameters") } dict := make(map[string]interface{}, len(values)/2) for i := 0; i < len(values); i += 2 { key, ok := values[i].(string) if !ok { return nil, errors.New("dict: keys must be strings") } dict[key] = values[i+1] } return dict, nil } diff --git a/templates/bare.tmpl b/templates/bare.tmpl index c2fef0f..7f06152 100644 --- a/templates/bare.tmpl +++ b/templates/bare.tmpl @@ -1,266 +1,267 @@ {{define "pad"}} + {{template "head-base" .}} {{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} — {{.SiteName}} - - {{if .CustomCSS}}{{end}} + + {{if .CustomCSS}}{{end}}
- {{if not .SingleUser}}

{{if .Chorus}}{{else}}{{end}}{{.SiteName}}

{{end}} + {{if not .SingleUser}}

{{if .Chorus}}{{else}}{{end}}{{.SiteName}}

{{end}}
- {{if .Editing}}{{end}} + {{if .Editing}}{{end}}
- + {{end}} diff --git a/templates/base.tmpl b/templates/base.tmpl index 4440860..90f6370 100644 --- a/templates/base.tmpl +++ b/templates/base.tmpl @@ -1,95 +1,96 @@ {{define "base"}} + {{template "head-base" .}} {{ template "head" . }} - - {{if .CustomCSS}}{{end}} - + + {{if .CustomCSS}}{{end}} +
{{ if .Chorus }} {{end}}
{{ template "content" . }}
{{ template "footer" . }} {{if not .JSDisabled}} - + {{else}} - {{if .WebFonts}}{{end}} + {{if .WebFonts}}{{end}} {{end}} {{end}} {{define "body-attrs"}}{{end}} diff --git a/templates/chorus-collection-post.tmpl b/templates/chorus-collection-post.tmpl index 468455c..b2e3319 100644 --- a/templates/chorus-collection-post.tmpl +++ b/templates/chorus-collection-post.tmpl @@ -1,155 +1,156 @@ {{define "post"}} + {{template "head-base" .}} {{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{.Collection.DisplayTitle}} - - {{if .CustomCSS}}{{end}} - + + {{if .CustomCSS}}{{end}} + {{if gt .Views 1}} {{end}} {{if gt (len .Images) 0}}{{else}}{{end}} {{range .Images}}{{else}}{{end}} {{template "collection-meta" .}} {{if .Collection.StyleSheet}}{{end}} {{if .Collection.RenderMathJax}} {{template "mathjax" . }} {{end}} {{template "highlighting" .}}
{{template "user-navigation" .}} {{if .Silenced}} {{template "user-silenced"}} {{end}}
{{if .IsScheduled}}

Scheduled

{{end}}{{if .Title.String}}

{{.FormattedDisplayTitle}}

{{end}}{{if and $.Collection.Format.ShowDates (not .IsPinned)}}{{end}}
{{.HTMLContent}}
{{ if .Collection.ShowFooterBranding }} {{ end }} {{if .Collection.CanShowScript}} {{range .Collection.ExternalScripts}}{{end}} {{if .Collection.Script}}{{end}} {{end}} - + {{if and .Monetization (not .IsOwner)}} - + {{end}} {{end}} diff --git a/templates/chorus-collection.tmpl b/templates/chorus-collection.tmpl index 742c544..1095de9 100644 --- a/templates/chorus-collection.tmpl +++ b/templates/chorus-collection.tmpl @@ -1,236 +1,237 @@ {{define "collection"}} + {{template "head-base" .}} {{.DisplayTitle}}{{if not .SingleUser}} — {{.SiteName}}{{end}} - - {{if .CustomCSS}}{{end}} - + + {{if .CustomCSS}}{{end}} + {{if gt .CurrentPage 1}}{{end}} {{if lt .CurrentPage .TotalPages}}{{end}} {{if not .IsPrivate}}{{end}} {{template "collection-meta" .}} {{if .StyleSheet}}{{end}} {{if .RenderMathJax}} {{template "mathjax" .}} {{end}} {{template "highlighting" . }} {{template "user-navigation" .}} {{if .Silenced}} {{template "user-silenced"}} {{end}}
-

{{.DisplayTitle}}

+

{{.DisplayTitle}}

{{if .Description}}

{{.Description}}

{{end}} {{/*if not .Public/*}} {{/*end*/}} {{if .PinnedPosts}} {{end}}
{{if .Posts}}
{{else}}
{{end}} {{if .IsWelcome}}

Welcome, {{.Username}}!

This is your new blog.

-

Start writing, or customize your blog.

-

Check out our writing guide to see what else you can do, and get in touch anytime with questions or feedback.

+

Start writing, or customize your blog.

+

Check out our writing guide to see what else you can do, and get in touch anytime with questions or feedback.

{{end}} {{template "posts" .}} {{if gt .TotalPages 1}}{{end}} {{if .Posts}}
{{else}}
{{end}} {{if .ShowFooterBranding }} {{ end }} {{if .CanShowScript}} {{range .ExternalScripts}}{{end}} {{if .Script}}{{end}} {{end}} - - - + + + {{end}} diff --git a/templates/classic.tmpl b/templates/classic.tmpl index 0c411b4..0033f23 100644 --- a/templates/classic.tmpl +++ b/templates/classic.tmpl @@ -1,408 +1,403 @@ {{define "pad"}} + {{template "head-base" .}} {{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} — {{.SiteName}} - - - {{if .CustomCSS}}{{end}} + + + {{if .CustomCSS}}{{end}}
- {{if not .SingleUser}}

{{end}} + {{if not .SingleUser}}

{{end}}
- {{if .Editing}}{{end}} - -
-
+ {{if .Editing}}{{end}} + +
+
- + - - + + {{end}} diff --git a/templates/collection-archive.tmpl b/templates/collection-archive.tmpl index 5256c58..57a414b 100644 --- a/templates/collection-archive.tmpl +++ b/templates/collection-archive.tmpl @@ -1,118 +1,119 @@ {{define "collection"}} + {{template "head-base" .}} Archive — {{.Collection.DisplayTitle}} - - {{if .CustomCSS}}{{end}} - + + {{if .CustomCSS}}{{end}} + {{if gt .CurrentPage 1}}{{end}} {{if lt .CurrentPage .TotalPages}}{{end}} {{if not .IsPrivate}}{{end}} {{template "collection-meta" .}} {{if .StyleSheet}}{{end}}
-

{{.Collection.DisplayTitle}}

+

{{.Collection.DisplayTitle}}

{{if .Posts -}}
{{- else -}}
{{- end}}

Archive

{{if .Flash}}

{{.Flash}}

{{end}} {{template "paging" .}} {{if .Posts}}
{{else}}
{{end}} {{if .ShowFooterBranding }} {{ end }} {{if .CanShowScript}} {{range .ExternalScripts}}{{end}} {{if .Collection.Script}}{{end}} {{end}} - + {{end}} diff --git a/templates/collection-post.tmpl b/templates/collection-post.tmpl index 3d3132d..5197be0 100644 --- a/templates/collection-post.tmpl +++ b/templates/collection-post.tmpl @@ -1,146 +1,147 @@ {{define "post"}} + {{template "head-base" .}} {{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{.Collection.DisplayTitle}} - - {{if .CustomCSS}}{{end}} - + + {{if .CustomCSS}}{{end}} + {{ if .IsFound }} {{if gt .Views 1}} {{end}} {{if gt (len .Images) 0}}{{else}}{{end}} {{range .Images}}{{else}}{{end}} {{ end }} {{template "collection-meta" .}} {{if .Collection.StyleSheet}}{{end}} {{if .Collection.RenderMathJax}} {{template "mathjax" . }} {{end}} {{template "highlighting" .}}
-

+

{{if .Silenced}} {{template "user-silenced"}} {{end}}
{{if .IsScheduled}}

Scheduled

{{end}}{{if .Title.String}}

{{.FormattedDisplayTitle}}

{{end}}{{if and $.Collection.Format.ShowDates (not .IsPinned) .IsFound}}{{end}}
{{.HTMLContent}}
{{ if .Collection.ShowFooterBranding }}

{{ end }} {{if .Collection.CanShowScript}} {{range .Collection.ExternalScripts}}{{end}} {{if .Collection.Script}}{{end}} {{end}} - + {{if and .Monetization (not .IsOwner)}} - + {{end}} {{end}} diff --git a/templates/collection-tags.tmpl b/templates/collection-tags.tmpl index c8e8a12..915a341 100644 --- a/templates/collection-tags.tmpl +++ b/templates/collection-tags.tmpl @@ -1,211 +1,212 @@ {{define "collection-tags"}} + {{template "head-base" .}} {{.Tag}} — {{.Collection.DisplayTitle}} - - {{if .CustomCSS}}{{end}} - + + {{if .CustomCSS}}{{end}} + {{if not .Collection.IsPrivate}}{{end}} {{if gt .Views 1}} {{end}} {{template "collection-meta" .}} {{if .Collection.StyleSheet}}{{end}} {{if .Collection.RenderMathJax}} {{template "mathjax" .}} {{end}} {{template "highlighting" . }}
-

{{.Collection.DisplayTitle}}

+

{{.Collection.DisplayTitle}}

{{if .Silenced}} {{template "user-silenced"}} {{end}} {{if .Posts}}
{{else}}
{{end}}

{{.Tag}}

{{template "posts" .}} {{if gt .TotalPages 1}}{{end}} {{if .Posts}}
{{else}}
{{end}} {{ if .Collection.ShowFooterBranding }} {{ end }} {{if .CanShowScript}} {{range .ExternalScripts}}{{end}} {{if .Collection.Script}}{{end}} {{end}} - + {{if .IsOwner}} - - + + {{end}} {{end}} diff --git a/templates/collection.tmpl b/templates/collection.tmpl index 269004e..13ad848 100644 --- a/templates/collection.tmpl +++ b/templates/collection.tmpl @@ -1,261 +1,262 @@ {{define "collection"}} + {{template "head-base" .}} {{.DisplayTitle}}{{if not .SingleUser}} — {{.SiteName}}{{end}} - - {{if .CustomCSS}}{{end}} - + + {{if .CustomCSS}}{{end}} + {{if gt .CurrentPage 1}}{{end}} {{if lt .CurrentPage .TotalPages}}{{end}} {{if not .IsPrivate}}{{end}} {{template "collection-meta" .}} {{if .StyleSheet}}{{end}} {{if .RenderMathJax}} {{template "mathjax" .}} {{end}} {{template "highlighting" . }} {{if or .IsOwner .SingleUser}} {{else if .IsCollLoggedIn}} {{end}}
{{if .Silenced}} {{template "user-silenced"}} {{end}} -

{{if .Posts}}{{else}}write.as {{end}}{{.DisplayTitle}}

+

{{if .Posts}}{{else}}write.as {{end}}{{.DisplayTitle}}

{{if .Description}}

{{.DisplayDescription}}

{{end}} {{/*if not .Public/*}} {{/*end*/}} {{if .PinnedPosts}} {{end}}
{{if .Posts}}
{{else}}
{{end}} {{if .IsWelcome}}

Welcome, {{.Username}}!

This is your new blog.

-

Start writing, or customize your blog.

-

Check out our writing guide to see what else you can do, and get in touch anytime with questions or feedback.

+

Start writing, or customize your blog.

+

Check out our writing guide to see what else you can do, and get in touch anytime with questions or feedback.

{{end}} {{if .Flash}}

{{.Flash}}

{{end}} {{template "posts" .}} {{if gt .TotalPages 1}}{{end}} {{if not .IsWelcome}}{{template "emailsubscribe" .}}{{end}} {{if .Posts}}
{{else}}
{{end}} {{if .ShowFooterBranding }} {{ end }} {{if .CanShowScript}} {{range .ExternalScripts}}{{end}} {{if .Script}}{{end}} {{end}} - - - - + + + + {{end}} diff --git a/templates/edit-meta.tmpl b/templates/edit-meta.tmpl index 6c964b6..321c11a 100644 --- a/templates/edit-meta.tmpl +++ b/templates/edit-meta.tmpl @@ -1,379 +1,376 @@ {{define "edit-meta"}} + {{template "head-base" .}} Edit metadata: {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}} — {{.SiteName}} - - - {{if .CustomCSS}}{{end}} + + + {{if .CustomCSS}}{{end}} - +
-

+

-
-
-
+
+
+
- +
-
-

Edit metadata: {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}} view post

+ +

Edit metadata: {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}} view post

{{if .Flashes}}
    {{range .Flashes}}
  • {{.}}
  • {{end}}
{{end}}
{{if .EditCollection}}
{{end}}
- UTC now + UTC now

Date format should be: YYYY-MM-DD HH:MM:SS

-
-
- -
 
- - + + - + {{end}} diff --git a/templates/include/footer.tmpl b/templates/include/footer.tmpl index 8e3dec9..3cb8b59 100644 --- a/templates/include/footer.tmpl +++ b/templates/include/footer.tmpl @@ -1,46 +1,46 @@ {{define "footer"}}
{{if or .SingleUser .WFModesty}} {{else}}
-

{{.SiteName}}

+

{{.SiteName}}

{{end}} {{end}} diff --git a/templates/include/head-base.tmpl b/templates/include/head-base.tmpl new file mode 100644 index 0000000..83b0d3f --- /dev/null +++ b/templates/include/head-base.tmpl @@ -0,0 +1 @@ +{{define "head-base"}}{{end}} \ No newline at end of file diff --git a/templates/include/oauth.tmpl b/templates/include/oauth.tmpl index 9a8d05e..f3a7420 100644 --- a/templates/include/oauth.tmpl +++ b/templates/include/oauth.tmpl @@ -1,37 +1,37 @@ {{define "oauth-buttons"}} {{ if or .SlackEnabled .WriteAsEnabled .GitLabEnabled .GiteaEnabled .GenericEnabled }}
{{ if .SlackEnabled }} - Sign in with Slack + Sign in with Slack {{ end }} {{ if .WriteAsEnabled }} - - + + Sign in with Write.as {{ end }} {{ if .GitLabEnabled }} - - + + Sign in with {{.GitLabDisplayName}} {{ end }} {{ if .GiteaEnabled }} - - + + Sign in with {{.GiteaDisplayName}} {{ end }} {{ if .GenericEnabled }} - Sign in with {{.GenericDisplayName}} + Sign in with {{.GenericDisplayName}} {{ end }}
{{if not .DisablePasswordAuth}}

or


{{end}} {{ end }} {{end}} \ No newline at end of file diff --git a/templates/include/post-render.tmpl b/templates/include/post-render.tmpl index e425e25..fc8e749 100644 --- a/templates/include/post-render.tmpl +++ b/templates/include/post-render.tmpl @@ -1,133 +1,133 @@ {{define "collection-meta"}} {{if .Monetization -}} {{- end}} {{if .Verification -}} {{if .FediverseAuthor}}{{end}} {{- end}} {{end}} {{define "highlighting"}} {{end}} {{define "mathjax"}} - {{end}} {{define "emailsubscribe"}} {{if .EmailSubsEnabled}}
{{if .IsSubscriber}} -

You're subscribed to email updates. Unsubscribe.

+

You're subscribed to email updates. Unsubscribe.

{{else}} -
+

Enter your email to subscribe to updates.

{{end}}
{{end}} {{end}} diff --git a/templates/include/posts.tmpl b/templates/include/posts.tmpl index 6a0d847..717a6e3 100644 --- a/templates/include/posts.tmpl +++ b/templates/include/posts.tmpl @@ -1,76 +1,75 @@ {{ define "posts" }} {{ range $el := .Posts }}
{{if .IsScheduled}}

Scheduled

{{end}} {{if .Title.String}}

{{- if .HasTitleLink -}} - {{.HTMLTitle}} + {{.HTMLTitle}} {{- else -}} {{- if .IsPaid}}{{template "paid-badge" .}}{{end -}} - + {{- end}} {{if $.IsOwner}} - - {{if $.CanPin}}{{end}} - + + {{if $.CanPin}}{{end}} + {{if gt (len $.Collections) 1}}{{else}} {{range $.Collections}} - + {{end}} {{end}} {{end}}

{{if $.Format.ShowDates}}{{end}} {{else}}

{{if $.Format.ShowDates -}} {{- if .IsPaid}}{{template "paid-badge" .}}{{end -}} - + {{- end}} {{if $.IsOwner}} - {{if not $.Format.ShowDates}}{{end}} - - {{if $.CanPin}}{{end}} - + {{if not $.Format.ShowDates}}{{end}} + + {{if $.CanPin}}{{end}} + {{if gt (len $.Collections) 1}}{{else}} {{range $.Collections}} - + {{end}} {{end}} {{end}}

{{end}} -{{if .Excerpt}}
{{if and (and (not $.IsOwner) (not $.Format.ShowDates)) (not .Title.String)}}{{end}}{{.Excerpt}}
- -{{localstr "Read more..." .Language.String}}{{else}}
{{if and (and (not $.IsOwner) (not $.Format.ShowDates)) (not .Title.String)}}{{end}}{{.HTMLContent}}
{{end}}
{{ end }} +{{if .Excerpt}}
{{if and (and (not $.IsOwner) (not $.Format.ShowDates)) (not .Title.String)}}{{end}}{{.Excerpt}}
+{{localstr "Read more..." .Language.String}}{{else}}
{{if and (and (not $.IsOwner) (not $.Format.ShowDates)) (not .Title.String)}}{{end}}{{.HTMLContent}}
{{end}}{{ end }} {{ end }} -{{define "paid-badge"}} {{end}} +{{define "paid-badge"}} {{end}} {{define "paging"}} {{if gt .TotalPages 1}}{{end}} {{end}} diff --git a/templates/pad.tmpl b/templates/pad.tmpl index 079359d..d57e665 100644 --- a/templates/pad.tmpl +++ b/templates/pad.tmpl @@ -1,463 +1,422 @@ {{define "pad"}} + {{template "head-base" .}} {{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} — {{.SiteName}} - - - {{if .CustomCSS}}{{end}} + + + {{if .CustomCSS}}{{end}}
- - - + - +
- {{if not .SingleUser}}

{{end}} + {{if not .SingleUser}}

{{end}}
- {{if .Editing}}{{end}} - -
-
-
+ {{if .Editing}}{{end}} + +
+
- - - + + - + {{end}} diff --git a/templates/password-collection.tmpl b/templates/password-collection.tmpl index fde4edc..f6a6dbc 100644 --- a/templates/password-collection.tmpl +++ b/templates/password-collection.tmpl @@ -1,88 +1,89 @@ {{define "password-collection"}} + {{template "head-base" .}} {{.DisplayTitle}}{{if not .SingleUser}} — {{.SiteName}}{{end}} - - {{if .CustomCSS}}{{end}} - + + {{if .CustomCSS}}{{end}} + {{if .StyleSheet}}{{end}} {{if .SingleUser}} {{end}}
-

{{.DisplayTitle}}

+

{{.DisplayTitle}}

-
+ {{if .Flashes}}
    {{range .Flashes}}
  • {{.}}
  • {{end}}
{{else}}

This blog requires a password.

{{end}}

{{if and .Script .CanShowScript}}{{end}} - - + + {{end}} diff --git a/templates/post.tmpl b/templates/post.tmpl index ca1c1f9..593d73e 100644 --- a/templates/post.tmpl +++ b/templates/post.tmpl @@ -1,102 +1,103 @@ {{define "post"}} + {{template "head-base" .}} {{if .Title}}{{.Title}}{{else}}{{.GenTitle}}{{end}} {{localhtml "title dash" .Language}} {{.SiteName}} {{if .IsCode}} - + {{end}} - - {{if .CustomCSS}}{{end}} - + + {{if .CustomCSS}}{{end}} + {{if gt .Views 1}} {{end}} {{if gt (len .Images) 0}}{{else}}{{end}} {{range .Images}}{{else}}{{end}} {{if .Author}}{{end}} {{template "highlighting" .}}
-

{{.SiteName}}

+

{{.SiteName}}

{{if .Silenced}} {{template "user-silenced"}} {{end}}
{{if .Title}}

{{.Title}}

{{end}}{{ if .IsPlainText }}

{{.Content}}

{{ else }}
{{.HTMLContent}}
{{ end }}

{{if .IsCode}} - + {{else}} - - {{if .IsPlainText}}{{end}} + + {{if .IsPlainText}}{{end}} {{end}} {{end}} diff --git a/templates/read.tmpl b/templates/read.tmpl index c032970..79f7a1c 100644 --- a/templates/read.tmpl +++ b/templates/read.tmpl @@ -1,142 +1,142 @@ {{define "head"}}{{.SiteName}} Reader - + {{if gt .CurrentPage 1}}{{end}} {{if lt .CurrentPage .TotalPages}}{{end}} {{end}} {{define "body-attrs"}}id="collection"{{end}} {{define "content"}}

{{.ContentTitle}}

{{if .SelTopic}}#{{.SelTopic}} posts{{else}}{{.Content}}{{end}}

{{ if gt (len .Posts) 0 }}
{{range .Posts}}
{{if .Title.String -}}

{{- if .IsPaid}}{{template "paid-badge" .}}{{end -}}

{{- else -}}

{{- if .IsPaid}}{{template "paid-badge" .}}{{end -}}

{{- end}}

{{if .Collection}}from {{.Collection.DisplayTitle}}{{else}}Anonymous{{end}}

{{if .Excerpt}}
{{.Excerpt}}
{{localstr "Read more..." .Language.String}}{{else}}
{{ if not .HTMLContent }}

{{.Content}}

{{ else }}{{.HTMLContent}}{{ end }}
 
{{localstr "Read more..." .Language.String}}{{end}}
{{end}}
{{ else }}

No posts here yet!

{{ end }} {{if gt .TotalPages 1}}{{end}}
- {{end}} diff --git a/templates/user/admin/app-settings.tmpl b/templates/user/admin/app-settings.tmpl index 50c50ec..bfdbd73 100644 --- a/templates/user/admin/app-settings.tmpl +++ b/templates/user/admin/app-settings.tmpl @@ -1,176 +1,176 @@ {{define "app-settings"}} {{template "header" .}}
{{template "admin-header" .}} {{if .Message}}

{{.Message}}

{{end}} {{if .ConfigMessage}}

{{.ConfigMessage}}

{{end}} -
+